@iamem/amem 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/skills.js ADDED
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Procedural memory. Claims are small facts that ride in every packet; a skill is a
3
+ * longer procedure that should only load when it is relevant.
4
+ *
5
+ * Files on disk are the source of truth (`~/.amem/skills/<name>/SKILL.md`), because every
6
+ * other agent tool in this ecosystem — Cursor, Claude, Hermes — discovers skills by
7
+ * scanning folders, and a skill's `references/` and `scripts/` do not fit in a column.
8
+ * SQLite only indexes them for ranking and usage stats; see `src/db.ts`.
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
12
+ import { join, resolve, sep } from "node:path";
13
+ import { pruneSkillRows, upsertSkillRow } from "./db.js";
14
+ import { amemHome } from "./paths.js";
15
+ import { compiledDenyPatterns } from "./policy.js";
16
+ import { tokenize } from "./search.js";
17
+ export const SKILL_FILE = "SKILL.md";
18
+ /** Subdirectories a skill may carry, matching the agentskills.io layout. */
19
+ export const SKILL_ASSET_DIRS = ["references", "templates", "scripts", "examples", "assets"];
20
+ export function skillsDir() {
21
+ return join(amemHome(), "skills");
22
+ }
23
+ export function ensureSkillsDir() {
24
+ const dir = skillsDir();
25
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
26
+ return dir;
27
+ }
28
+ export function hashSkillContent(content) {
29
+ return createHash("sha256").update(content, "utf8").digest("hex").slice(0, 16);
30
+ }
31
+ /**
32
+ * Skill names become directory names and slash commands, so keep them to the identifier
33
+ * shape the ecosystem uses and never let one escape the skills directory.
34
+ */
35
+ export function slugifySkillName(raw) {
36
+ const slug = String(raw || "")
37
+ .trim()
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9_-]+/g, "-")
40
+ .replace(/^-+|-+$/g, "")
41
+ .slice(0, 64);
42
+ return slug;
43
+ }
44
+ export function isValidSkillName(raw) {
45
+ return /^[a-z][a-z0-9_-]*$/.test(String(raw || "")) && String(raw).length <= 64;
46
+ }
47
+ /**
48
+ * Minimal YAML-frontmatter reader — enough for the scalar and inline-list keys skills
49
+ * actually use. Nested keys are flattened to their leaf (`metadata.hermes.tags` -> `tags`)
50
+ * so a Hermes-authored skill and a Cursor-authored one both parse.
51
+ */
52
+ export function parseFrontmatter(raw) {
53
+ const text = String(raw ?? "").replace(/^\uFEFF/, "");
54
+ const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
55
+ if (!match)
56
+ return { meta: {}, body: text.trim() };
57
+ const meta = {};
58
+ for (const line of match[1].split(/\r?\n/)) {
59
+ if (!line.trim() || line.trim().startsWith("#"))
60
+ continue;
61
+ const kv = /^\s*([A-Za-z0-9_.-]+)\s*:\s*(.*)$/.exec(line);
62
+ if (!kv)
63
+ continue;
64
+ const key = kv[1].split(".").pop().toLowerCase();
65
+ const value = kv[2].trim();
66
+ if (!value)
67
+ continue; // a bare `metadata:` parent carries nothing itself
68
+ if (value.startsWith("[") && value.endsWith("]")) {
69
+ meta[key] = value
70
+ .slice(1, -1)
71
+ .split(",")
72
+ .map((v) => stripQuotes(v.trim()))
73
+ .filter(Boolean);
74
+ }
75
+ else {
76
+ meta[key] = stripQuotes(value);
77
+ }
78
+ }
79
+ return { meta, body: text.slice(match[0].length).trim() };
80
+ }
81
+ function stripQuotes(value) {
82
+ const v = value.trim();
83
+ if (v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))) {
84
+ return v.slice(1, -1);
85
+ }
86
+ return v;
87
+ }
88
+ function metaString(meta, key) {
89
+ const v = meta[key];
90
+ if (typeof v === "string" && v.trim())
91
+ return v.trim();
92
+ return null;
93
+ }
94
+ function metaList(meta, key) {
95
+ const v = meta[key];
96
+ if (Array.isArray(v))
97
+ return v.filter((s) => typeof s === "string" && s.trim()).map((s) => s.trim());
98
+ if (typeof v === "string" && v.trim())
99
+ return v.split(/[,\s]+/).filter(Boolean);
100
+ return [];
101
+ }
102
+ /** First markdown heading, used when a skill has no `description:` to show. */
103
+ function firstHeading(body) {
104
+ for (const line of body.split(/\r?\n/)) {
105
+ const h = /^#{1,3}\s+(.+?)\s*$/.exec(line);
106
+ if (h)
107
+ return h[1].trim();
108
+ }
109
+ return "";
110
+ }
111
+ export function readSkillMeta(dir, source = "local") {
112
+ const file = join(dir, SKILL_FILE);
113
+ if (!existsSync(file) || !statSync(file).isFile())
114
+ return null;
115
+ const raw = readFileSync(file, "utf8");
116
+ const { meta, body } = parseFrontmatter(raw);
117
+ // Name resolution mirrors the ecosystem: frontmatter first, then the folder name.
118
+ const declared = metaString(meta, "name");
119
+ const name = isValidSkillName(declared || "") ? declared : slugifySkillName(dir.split(sep).pop() || "");
120
+ if (!name)
121
+ return null;
122
+ return {
123
+ name,
124
+ description: metaString(meta, "description") || firstHeading(body),
125
+ version: metaString(meta, "version"),
126
+ tags: metaList(meta, "tags"),
127
+ path: file,
128
+ dir,
129
+ hash: hashSkillContent(raw),
130
+ source,
131
+ };
132
+ }
133
+ /** Every skill on disk, sorted by name. Skips dot/underscore dirs like the hub state. */
134
+ export function scanSkills(root = skillsDir()) {
135
+ if (!existsSync(root))
136
+ return [];
137
+ const out = [];
138
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
139
+ if (!entry.isDirectory())
140
+ continue;
141
+ if (entry.name.startsWith(".") || entry.name.startsWith("_"))
142
+ continue;
143
+ const dir = join(root, entry.name);
144
+ const meta = readSkillMeta(dir);
145
+ if (meta) {
146
+ out.push(meta);
147
+ continue;
148
+ }
149
+ // One level of category nesting, the way Hermes groups skills (mlops/axolotl).
150
+ for (const child of readdirSync(dir, { withFileTypes: true })) {
151
+ if (!child.isDirectory() || child.name.startsWith("."))
152
+ continue;
153
+ const nested = readSkillMeta(join(dir, child.name));
154
+ if (nested)
155
+ out.push(nested);
156
+ }
157
+ }
158
+ return out.sort((a, b) => a.name.localeCompare(b.name));
159
+ }
160
+ export function findSkillOnDisk(name, root = skillsDir()) {
161
+ const slug = slugifySkillName(name);
162
+ if (!slug)
163
+ return null;
164
+ return scanSkills(root).find((s) => s.name === slug) ?? null;
165
+ }
166
+ export function skillDirFor(name, root = skillsDir()) {
167
+ const slug = slugifySkillName(name);
168
+ if (!isValidSkillName(slug))
169
+ throw new Error(`Invalid skill name: ${name}`);
170
+ return join(root, slug);
171
+ }
172
+ export function readSkillBody(name, root = skillsDir()) {
173
+ const meta = findSkillOnDisk(name, root);
174
+ return meta ? readFileSync(meta.path, "utf8") : null;
175
+ }
176
+ /**
177
+ * Read a supporting file (`references/foo.md`). Skills come from other people, so the
178
+ * path is resolved and re-checked rather than trusted.
179
+ */
180
+ export function readSkillAsset(name, relPath, root = skillsDir()) {
181
+ const meta = findSkillOnDisk(name, root);
182
+ if (!meta)
183
+ return null;
184
+ const base = resolve(meta.dir);
185
+ const target = resolve(base, relPath);
186
+ // resolve() collapses "..", and an absolute relPath lands outside base — both caught here.
187
+ if (target !== base && !target.startsWith(base + sep))
188
+ return null;
189
+ if (!existsSync(target) || !statSync(target).isFile())
190
+ return null;
191
+ return readFileSync(target, "utf8");
192
+ }
193
+ export function listSkillAssets(name, root = skillsDir()) {
194
+ const meta = findSkillOnDisk(name, root);
195
+ if (!meta)
196
+ return [];
197
+ const out = [];
198
+ for (const sub of SKILL_ASSET_DIRS) {
199
+ const dir = join(meta.dir, sub);
200
+ if (!existsSync(dir) || !statSync(dir).isDirectory())
201
+ continue;
202
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
203
+ if (entry.isFile())
204
+ out.push(`${sub}/${entry.name}`);
205
+ }
206
+ }
207
+ return out.sort();
208
+ }
209
+ /** Render a SKILL.md from parts, for `amem skills new` and agent-authored saves. */
210
+ export function renderSkillMarkdown(input) {
211
+ const lines = ["---", `name: ${input.name}`, `description: ${input.description}`];
212
+ if (input.version)
213
+ lines.push(`version: ${input.version}`);
214
+ if (input.tags?.length)
215
+ lines.push(`tags: [${input.tags.join(", ")}]`);
216
+ lines.push("---", "");
217
+ const body = (input.body || "").trim();
218
+ lines.push(body || defaultSkillBody(input.name));
219
+ return `${lines.join("\n")}\n`;
220
+ }
221
+ function defaultSkillBody(name) {
222
+ const title = name.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
223
+ return [
224
+ `# ${title}`,
225
+ "",
226
+ "## When to use",
227
+ "",
228
+ "Describe the trigger — the situation where this procedure applies.",
229
+ "",
230
+ "## Procedure",
231
+ "",
232
+ "1. First step.",
233
+ "2. Second step.",
234
+ "",
235
+ "## Pitfalls",
236
+ "",
237
+ "- Known failure modes and how to get past them.",
238
+ "",
239
+ "## Verification",
240
+ "",
241
+ "How to confirm it worked.",
242
+ ].join("\n");
243
+ }
244
+ export function writeSkill(name, content, root = skillsDir()) {
245
+ const slug = slugifySkillName(name);
246
+ if (!isValidSkillName(slug))
247
+ throw new Error(`Invalid skill name: ${name}`);
248
+ const dir = join(root, slug);
249
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
250
+ const path = join(dir, SKILL_FILE);
251
+ const body = content.endsWith("\n") ? content : `${content}\n`;
252
+ writeFileSync(path, body, { mode: 0o600 });
253
+ return { name: slug, path, hash: hashSkillContent(body) };
254
+ }
255
+ export function deleteSkill(name, root = skillsDir()) {
256
+ const meta = findSkillOnDisk(name, root);
257
+ if (!meta)
258
+ return false;
259
+ rmSync(meta.dir, { recursive: true, force: true });
260
+ return true;
261
+ }
262
+ /**
263
+ * Prompt-injection shapes that have no business in a stored procedure. A skill is riskier
264
+ * than a claim: a claim is a fact the agent reads, a skill is an instruction it follows.
265
+ */
266
+ const SKILL_INJECTION_PATTERNS = [
267
+ {
268
+ re: /ignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions/i,
269
+ reason: "prompt-injection directive",
270
+ },
271
+ { re: /disregard\s+(?:your\s+)?(?:system\s+prompt|safety|guidelines)/i, reason: "safety override" },
272
+ { re: /\bcurl\b[^\n]*\|\s*(?:ba)?sh\b/i, reason: "pipes a remote script into a shell" },
273
+ { re: /rm\s+-rf\s+[~/]\s*(?:$|[^\w/])/im, reason: "destructive filesystem command" },
274
+ {
275
+ re: /(?:curl|wget|fetch)[^\n]*(?:AWS_|API_KEY|SECRET|TOKEN|\.env\b|id_rsa)/i,
276
+ reason: "looks like credential exfiltration",
277
+ },
278
+ ];
279
+ /**
280
+ * Gate content before it lands in the library. Deny patterns come from policy so an IT
281
+ * operator's additions apply to skills too, not just claims.
282
+ */
283
+ export function scanSkillContent(content, denyPatterns) {
284
+ const text = String(content ?? "");
285
+ if (!text.trim())
286
+ return { ok: false, reason: "empty skill content" };
287
+ for (const { re, reason } of SKILL_INJECTION_PATTERNS) {
288
+ if (re.test(text))
289
+ return { ok: false, reason };
290
+ }
291
+ const deny = denyPatterns ?? compiledDenyPatterns();
292
+ for (const re of deny) {
293
+ if (re.test(text))
294
+ return { ok: false, reason: `matches deny pattern ${re.source}` };
295
+ }
296
+ return { ok: true };
297
+ }
298
+ /**
299
+ * Import a skill from a local directory. Local paths only — no registries and no network,
300
+ * which keeps this on the right side of the "no cloud, no hosted anything" line.
301
+ * Supporting files come along, but only from the allowlisted asset directories.
302
+ */
303
+ export function importSkillFromPath(sourcePath, overrideName, root = skillsDir()) {
304
+ const src = resolve(sourcePath);
305
+ if (!existsSync(src))
306
+ throw new Error(`No such path: ${sourcePath}`);
307
+ const srcDir = statSync(src).isDirectory() ? src : join(src, "..");
308
+ const skillFile = statSync(src).isDirectory() ? join(src, SKILL_FILE) : src;
309
+ if (!existsSync(skillFile))
310
+ throw new Error(`No ${SKILL_FILE} found at ${sourcePath}`);
311
+ const raw = readFileSync(skillFile, "utf8");
312
+ const scan = scanSkillContent(raw);
313
+ if (!scan.ok)
314
+ throw new Error(`Refusing to import: ${scan.reason}`);
315
+ const { meta } = parseFrontmatter(raw);
316
+ const declared = typeof meta.name === "string" ? meta.name : "";
317
+ const candidate = overrideName || declared || srcDir.split(sep).pop() || "";
318
+ const slug = slugifySkillName(candidate);
319
+ if (!isValidSkillName(slug)) {
320
+ throw new Error(`Could not derive a skill name from ${sourcePath} — pass --name`);
321
+ }
322
+ const written = writeSkill(slug, raw, root);
323
+ const destDir = join(root, slug);
324
+ const files = [];
325
+ for (const sub of SKILL_ASSET_DIRS) {
326
+ const from = join(srcDir, sub);
327
+ if (!existsSync(from) || !statSync(from).isDirectory())
328
+ continue;
329
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
330
+ if (!entry.isFile())
331
+ continue;
332
+ const target = join(destDir, sub, entry.name);
333
+ mkdirSync(join(destDir, sub), { recursive: true, mode: 0o700 });
334
+ writeFileSync(target, readFileSync(join(from, entry.name)), { mode: 0o600 });
335
+ files.push(`${sub}/${entry.name}`);
336
+ }
337
+ }
338
+ return { name: written.name, path: written.path, files };
339
+ }
340
+ /**
341
+ * Reconcile the index with disk. Cheap enough to run before any read, which keeps the
342
+ * index honest when a user or agent edits a SKILL.md with ordinary file tools.
343
+ */
344
+ export function syncSkillIndex(root = skillsDir()) {
345
+ const found = scanSkills(root);
346
+ const out = [];
347
+ for (const meta of found) {
348
+ const row = upsertSkillRow({
349
+ name: meta.name,
350
+ path: meta.path,
351
+ description: meta.description,
352
+ version: meta.version,
353
+ tags: meta.tags,
354
+ contentHash: meta.hash,
355
+ source: meta.source,
356
+ });
357
+ out.push({
358
+ ...meta,
359
+ repoId: row.repo_id,
360
+ uses: row.uses,
361
+ lastUsedAt: row.last_used_at,
362
+ modified: Boolean(row.origin_hash) && row.origin_hash !== meta.hash,
363
+ });
364
+ }
365
+ pruneSkillRows(found.map((s) => s.name));
366
+ return out;
367
+ }
368
+ export function listIndexedSkills(root = skillsDir()) {
369
+ return syncSkillIndex(root);
370
+ }
371
+ /**
372
+ * Rank skills for a query. Deliberately matches on the index fields only — name,
373
+ * description, tags — because the whole point is to decide what is worth loading
374
+ * without paying for the bodies.
375
+ */
376
+ export function skillSummary(s) {
377
+ return {
378
+ name: s.name,
379
+ description: s.description,
380
+ version: s.version,
381
+ tags: s.tags,
382
+ path: s.path,
383
+ dir: s.dir,
384
+ hash: s.hash,
385
+ source: s.source,
386
+ ...("repoId" in s ? { repoId: s.repoId, uses: s.uses, lastUsedAt: s.lastUsedAt, modified: s.modified } : {}),
387
+ };
388
+ }
389
+ export function rankSkills(skills, query, limit = 3) {
390
+ const tokens = tokenize(query);
391
+ if (tokens.length === 0)
392
+ return [];
393
+ const ranked = [];
394
+ for (const skill of skills) {
395
+ const name = skill.name.toLowerCase();
396
+ const haystack = `${name} ${skill.description} ${skill.tags.join(" ")}`.toLowerCase();
397
+ let score = 0;
398
+ const reasons = [];
399
+ let hits = 0;
400
+ for (const token of tokens) {
401
+ if (name.includes(token)) {
402
+ score += 6;
403
+ hits += 1;
404
+ }
405
+ else if (haystack.includes(token)) {
406
+ score += token.length > 4 ? 3 : 2;
407
+ hits += 1;
408
+ }
409
+ }
410
+ if (hits === 0)
411
+ continue;
412
+ reasons.push(`match+${score}`);
413
+ // A skill that keeps getting used is a better bet than one nobody has opened.
414
+ if (skill.uses > 0) {
415
+ const useBoost = Math.min(4, Math.log2(skill.uses + 1) * 2);
416
+ score += useBoost;
417
+ reasons.push(`used×${skill.uses}`);
418
+ }
419
+ ranked.push({ ...skill, score, reasons });
420
+ }
421
+ return ranked.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
422
+ }
package/docs/backlog.md CHANGED
@@ -4,6 +4,7 @@ Updated after completing the Feature Map **Later** phase (local embedding model
4
4
 
5
5
  ## Shipped recently
6
6
 
7
+ - **Agent Tasks Kanban** — per-project board + MCP `amem_task_*` + open tasks in context packets (complement to Memory facts)
7
8
  - FTS retrieval, claim staleness, supersede/conflict
8
9
  - Session-end **draft capture** + Memory approve/dismiss
9
10
  - Memory **edit / delete / pin / search**
@@ -37,6 +38,14 @@ Updated after completing the Feature Map **Later** phase (local embedding model
37
38
 
38
39
  ## Open
39
40
 
41
+ - **Skills in backup/restore** — `createBackup` copies only the DB file, so `~/.amem/skills/`
42
+ is lost on restore. Now sharper than before: the `skills` index, `skill_drafts`, and
43
+ `skill_uses` tables all survive a restore while the SKILL.md bodies they point at do not,
44
+ so a restored machine gets an index of skills that no longer exist. Needs a decision:
45
+ make backups an archive (DB + skills dir), or keep backups DB-only, prune the index on
46
+ restore, and document skills as separately versioned.
47
+ - **Skill drafts have no retention policy** — dismissed and applied rows accumulate. Memory
48
+ drafts have hygiene sweeps; skill drafts do not yet.
40
49
  - Prompt-pack before/after Stats benchmark; restore wizard polish; IT seat pack.
41
50
  - Decide one-time vs subscription (offline files cannot revoke on cancel unless you add `expires_at` and re-issue).
42
51
  - Optional vendored ONNX/MiniLM weights in a paid pack (external command is the local hook today).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamem/amem",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Local personal agent memory for Cursor and Claude Code. Private to your machine — never shared.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,100 @@
1
+ ---
2
+ description: Manage deferred tasks and Kanban lifecycle in local personal amem memory so work is preserved across sessions and available for context retrieval.
3
+ ---
4
+
5
+ # amem-tasks
6
+
7
+ Track deferred work, multi-step progress, and backlog items in local amem memory so tasks never get lost across chat sessions.
8
+
9
+ ## Privacy
10
+
11
+ Memory is personal and stored under `~/.amem` on this machine. Tasks stay local and are never committed to remote repositories.
12
+
13
+ ## When to Use
14
+
15
+ 1. **Multi-step work**: When working on complex tasks or features with 3+ steps, create and manage tasks to track progress.
16
+ 2. **Deferred work**: When user or agent identifies follow-up work ("do X later", "verify Y after deploy", "refactor Z next week"), add it to the project backlog.
17
+ 3. **Session continuity & handoff**: Before ending a session or moving to a new topic, park unfinished items on the board.
18
+ 4. **Context retrieval**: Check `amem_task_list` or context packet `## Open tasks` / `## Tasks` to review current and completed work.
19
+
20
+ ## Task Status Lifecycle
21
+
22
+ - `backlog` — Queued items, follow-ups, and ideas.
23
+ - `next` — Prioritized tasks ready to start soon.
24
+ - `doing` — Currently in progress (keep 1 active task at a time).
25
+ - `blocked` — Stalled on external feedback, bug, or dependency.
26
+ - `done` — Finished work (retained for context retrieval and history).
27
+
28
+ ## Available Tools
29
+
30
+ ### 1. MCP Tools (Cursor, Claude Code, Windsurf, Continue, Zed)
31
+
32
+ - **`amem_task_add`**
33
+ ```json
34
+ {
35
+ "title": "Add rate limiter middleware to /api/auth",
36
+ "body": "Protect login endpoints against brute-force attempts",
37
+ "status": "backlog",
38
+ "anchors": ["src/api/auth.ts"]
39
+ }
40
+ ```
41
+
42
+ - **`amem_task_list`**
43
+ ```json
44
+ {
45
+ "status": "doing",
46
+ "include_done": true
47
+ }
48
+ ```
49
+
50
+ - **`amem_task_update`**
51
+ ```json
52
+ {
53
+ "id": "task_1234abcd",
54
+ "status": "doing",
55
+ "body": "Updated progress notes..."
56
+ }
57
+ ```
58
+
59
+ - **`amem_task_complete`**
60
+ ```json
61
+ {
62
+ "id": "task_1234abcd"
63
+ }
64
+ ```
65
+
66
+ ### 2. CLI Commands (Terminal, Aider, Bash scripts)
67
+
68
+ - **List tasks**:
69
+ ```bash
70
+ amem task list
71
+ amem task list --status doing
72
+ amem task list --include-done --all
73
+ ```
74
+
75
+ - **Add task**:
76
+ ```bash
77
+ amem task add "Add rate limiter middleware" --body "Protect /api/auth" --status backlog --anchor src/api/auth.ts
78
+ ```
79
+
80
+ - **Update task**:
81
+ ```bash
82
+ amem task update <id> --status doing
83
+ ```
84
+
85
+ - **Complete task**:
86
+ ```bash
87
+ amem task complete <id>
88
+ ```
89
+
90
+ - **Delete task**:
91
+ ```bash
92
+ amem task delete <id>
93
+ ```
94
+
95
+ ## Instructions for Agents
96
+
97
+ 1. **Deconstruct**: When starting a non-trivial user request, add the plan items to `amem_task_add`.
98
+ 2. **Track**: Mark the current working task as `doing` with `amem_task_update`.
99
+ 3. **Complete**: When a task is finished, call `amem_task_complete` immediately. Completed tasks stay stored in local memory and are searchable in context retrieval.
100
+ 4. **Learn**: If completing the task revealed durable repository facts (constraints, gotchas, ownership), save them with `amem_remember` or `amem-update-working-memory`.
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: amem-write-skill
3
+ description: Write a durable procedure from this session into local amem skills.
4
+ ---
5
+
6
+ # amem-write-skill
7
+
8
+ Turn a non-trivial workflow you just worked out into a reusable skill stored in local amem.
9
+
10
+ Use this when an amem context packet shows a **Worth saving as a skill** or **Skill worth
11
+ revising** nudge, or when you notice on your own that you solved something worth repeating.
12
+
13
+ ## Privacy
14
+
15
+ - Skills are stored locally under `~/.amem/skills` and never leave this machine.
16
+ - Do not store secrets, tokens, credentials, connection strings, or private personal data.
17
+ amem scans content and will refuse writes that look like credentials.
18
+ - Do not paste proprietary company LLM instructions — write the procedure, not the prompt.
19
+
20
+ ## When to write one
21
+
22
+ Write a skill when at least one of these is true:
23
+
24
+ - You worked out a multi-step workflow that will come up again.
25
+ - You hit errors or dead ends and found the path that actually works.
26
+ - The user corrected your approach and the correction generalizes.
27
+
28
+ Do **not** write a skill for a one-off fix, a single command, or anything already obvious
29
+ from the repo's README. A small durable fact belongs in memory (`amem_remember`), not here.
30
+ The split: memory holds facts that should always be in context; skills hold procedures that
31
+ should load only when relevant.
32
+
33
+ ## Steps
34
+
35
+ 1. Check what already exists so you update instead of duplicating:
36
+
37
+ ```bash
38
+ amem skills list
39
+ ```
40
+
41
+ If a related skill exists, load it and revise it rather than writing a second one:
42
+
43
+ ```bash
44
+ amem skills show <name>
45
+ ```
46
+
47
+ 2. Draft the SKILL.md. Keep the description under about 80 characters — it is the only
48
+ thing agents see until they load the body, so it must say *when to use this*, not just
49
+ what it is.
50
+
51
+ 3. Save it with the `amem_skill_save` MCP tool, passing `name`, `description`, and
52
+ `content`. Prefer that over writing files directly so the content scan runs.
53
+
54
+ ## Format
55
+
56
+ ```markdown
57
+ ---
58
+ name: deploy-staging
59
+ description: Deploy the staging server and verify the health endpoint
60
+ tags: [deploy, staging]
61
+ ---
62
+
63
+ # Deploy Staging
64
+
65
+ ## When to use
66
+ Trigger conditions — the situation where this procedure applies.
67
+
68
+ ## Procedure
69
+ 1. Concrete step with the real command.
70
+ 2. Next step.
71
+
72
+ ## Pitfalls
73
+ - The dead end you hit, and what got you past it.
74
+
75
+ ## Verification
76
+ How to confirm it actually worked.
77
+ ```
78
+
79
+ ## Pitfalls
80
+
81
+ - **Vague descriptions.** "Helps with deploys" is useless for ranking. Name the trigger.
82
+ - **Transcribing the chat.** Write the procedure that worked, not the exploration that led
83
+ to it. Skip the wrong turns except as entries under Pitfalls.
84
+ - **Inventing steps.** Only include commands you actually ran and saw work.
85
+ - **Duplicates.** Revising an existing skill beats adding a near-copy.
86
+
87
+ ## Verification
88
+
89
+ ```bash
90
+ amem skills list # your skill appears with its description
91
+ amem skills show <name> # body reads as a procedure someone else could follow
92
+ ```
93
+
94
+ If a write is staged instead of saved, an approval gate is on. Review it with:
95
+
96
+ ```bash
97
+ amem skills drafts
98
+ amem skills approve <draft-id>
99
+ ```
@@ -1,23 +1,33 @@
1
1
  ---
2
- description: Use local personal amem memory before broad codebase exploration
2
+ description: Use local personal amem memory and tasks before broad codebase exploration
3
3
  globs:
4
4
  alwaysApply: true
5
5
  ---
6
6
 
7
7
  <!-- Generated by amem. Safe to commit: contains no memory contents. -->
8
8
 
9
- # amem local memory
9
+ # amem local memory & tasks
10
10
 
11
- amem injects matching local memory into Cursor automatically (session start + each prompt). Treat that packet as the first map of this repo.
11
+ amem injects matching local memory and open tasks into Cursor automatically (session start + each prompt). Treat that packet as the first map of this repo.
12
12
 
13
13
  1. Prefer amem file anchors over broad greps and multi-folder reads.
14
14
  2. Still verify current code before editing — memory can be stale. Trust **fresh** claims more; re-check anything marked **stale**.
15
15
  3. Use the **Why:** line as ranking explainability, not as proof.
16
- 4. After durable learnings, the stop hook queues a compact **session draft** (and may queue **miss→learn** drafts). Approve in `amem ui` → Memory, or run `amem-update-working-memory` for higher-quality facts.
17
- 5. Cross-repo personal prefs may appear with Why reason `personal` they are local “how I work” notes, not org wiki.
16
+ 4. **Agent Tasks & Kanban:**
17
+ - Use `amem_task_add` to record deferred work, follow-ups, or multi-step tasks so they don't get lost across chat sessions.
18
+ - Update tasks to `doing` with `amem_task_update` when starting work.
19
+ - Mark completed tasks with `amem_task_complete` as soon as finished so completed tasks are preserved in history and context retrieval.
20
+ - Query `amem_task_list` (or check `## Open tasks` in context) to see pending and completed tasks.
21
+ 5. **Skills (procedural memory):**
22
+ - A `## Relevant skills` section lists names and descriptions only. If one looks like it applies, call `amem_skill_view` to load the procedure **before** working it out yourself.
23
+ - When you solve something worth repeating — a multi-step workflow, a dead end you found the way past, or a correction the user gave you — save it with `amem_skill_save`. The `amem-write-skill` skill has the format.
24
+ - A `## Worth saving as a skill` or `## Skill worth revising` note in the packet means amem already spotted one. Write it up if you agree; it is a suggestion, not an order.
25
+ - Skills are procedures. Small durable facts still belong in memory via `amem_remember`.
26
+ 6. After durable learnings, the stop hook queues a compact **session draft** (and may queue **miss→learn** drafts). Approve in `amem ui` → Memory, or run `amem-update-working-memory` for higher-quality facts.
27
+ 7. Cross-repo personal prefs may appear with Why reason `personal` — they are local “how I work” notes, not org wiki.
18
28
 
19
29
  Do not re-run `amem context` unless the injected packet is empty or clearly wrong.
20
30
 
21
31
  Memory is personal and stored under `~/.amem` on this machine. Do not commit exports, backups, or database copies to shared remotes.
22
32
 
23
- Open the local UI anytime with `amem ui` (Setup, Memory drafts, Stats).
33
+ Open the local UI anytime with `amem ui` (Setup, Memory drafts, Tasks, Skills, Stats).