@xxkeefer/mrkl 0.2.14 → 0.3.1

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  <p align="center">
2
2
  <h1 align="center">mrkl</h1>
3
3
  <p align="center">
4
+ 📝 <i>mrkl, rhymes with sparkle</i> ✨
5
+ <br />
4
6
  Lightweight CLI for structured markdown task tracking.
5
7
  <br />
6
8
  Track work in your repo, not in a separate app.
@@ -15,17 +17,17 @@
15
17
 
16
18
  ---
17
19
 
18
- ## Why mrkl?
20
+ ## Why mrkl? 🤔
19
21
 
20
22
  Most task trackers live outside your codebase. mrkl keeps tasks as markdown files right alongside your code — version-controlled, greppable, and readable by both humans and AI agents.
21
23
 
22
- - **No external service** — tasks live in `.tasks/` as structured markdown
23
- - **Git-native** — commit, branch, and diff your tasks like any other file
24
- - **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
25
- - **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
26
- - **Zero config** — one command to set up, sensible defaults for everything
24
+ - 🗂️ **No external service** — tasks live in `.tasks/` as structured markdown
25
+ - 🌿 **Git-native** — commit, branch, and diff your tasks like any other file
26
+ - 🤖 **AI-agent friendly** — consistent YAML frontmatter makes tasks easy to parse programmatically
27
+ - 📏 **Conventional commits vocabulary** — task types mirror what you already use (`feat`, `fix`, `chore`, etc.)
28
+ - **Zero config** — one command to set up, sensible defaults for everything
27
29
 
28
- ## Install
30
+ ## Install 📦
29
31
 
30
32
  ```sh
31
33
  pnpm add -g @xxkeefer/mrkl
@@ -37,7 +39,7 @@ Or use without installing:
37
39
  npx @xxkeefer/mrkl init MY_PROJECT
38
40
  ```
39
41
 
40
- ## Quick Start
42
+ ## Quick Start 🚀
41
43
 
42
44
  ```sh
43
45
  # Initialize in your project root
@@ -60,9 +62,25 @@ mrkl list --status todo
60
62
 
61
63
  # Archive a completed task
62
64
  mrkl done PROJ-001
65
+
66
+ # All commands have short aliases
67
+ mrkl c feat "dark mode" # create
68
+ mrkl ls --type fix # list
69
+ mrkl d PROJ-001 # done
70
+ mrkl x PROJ-002 # close
63
71
  ```
64
72
 
65
- ## Commands
73
+ ## Commands 🛠️
74
+
75
+ | Command | Alias | Description |
76
+ |---------|-------|-------------|
77
+ | `init` | `i` | Initialize mrkl in the current project |
78
+ | `create` | `c` | Create a new task |
79
+ | `list` | `ls` | List active tasks |
80
+ | `done` | `d` | Mark a task as done and archive it |
81
+ | `close` | `x` | Close a task (won't do, duplicate, etc.) and archive it |
82
+ | `prune` | `p` | Delete archived tasks created on or before a given date |
83
+ | `install-skills` | — | Install bundled Claude Code skills |
66
84
 
67
85
  ### `mrkl init <prefix>`
68
86
 
@@ -89,10 +107,10 @@ Creates a new task file.
89
107
  | `type` | Task type (see [Task Types](#task-types)) |
90
108
  | `title` | Short description of the task |
91
109
 
92
- | Option | Description |
93
- |--------|-------------|
94
- | `--desc <text>` | Detailed description |
95
- | `--ac <text>` | Acceptance criterion (repeatable) |
110
+ | Option | Alias | Description |
111
+ |--------|-------|-------------|
112
+ | `--desc <text>` | `-d` | Detailed description |
113
+ | `--ac <text>` | `-a` | Acceptance criterion (repeatable) |
96
114
 
97
115
  ```sh
98
116
  mrkl create feat "search functionality" \
@@ -102,14 +120,16 @@ mrkl create feat "search functionality" \
102
120
  --ac "highlights matching terms"
103
121
  ```
104
122
 
123
+ Running `mrkl create` with no arguments enters **interactive mode**, prompting for type, title, description, and acceptance criteria.
124
+
105
125
  ### `mrkl list [options]`
106
126
 
107
127
  Lists all active tasks.
108
128
 
109
- | Option | Description |
110
- |--------|-------------|
111
- | `--type <type>` | Filter by task type |
112
- | `--status <status>` | Filter by status (`todo`, `in-progress`, `done`) |
129
+ | Option | Alias | Description |
130
+ |--------|-------|-------------|
131
+ | `--type <type>` | `-t` | Filter by task type |
132
+ | `--status <status>` | `-s` | Filter by status (`todo`, `in-progress`, `done`) |
113
133
 
114
134
  Non-conforming markdown files in the tasks directory are silently skipped.
115
135
 
@@ -123,7 +143,56 @@ Archives a completed task.
123
143
 
124
144
  Moves the task file to `.tasks/.archive/` and sets its status to `done`.
125
145
 
126
- ## Task Types
146
+ ### `mrkl close <id>`
147
+
148
+ Closes a task that won't be done — duplicates, out-of-scope work, etc.
149
+
150
+ | Argument | Description |
151
+ |----------|-------------|
152
+ | `id` | Task ID to close (e.g., `PROJ-002`) |
153
+
154
+ Sets the task status to `closed` and moves it to `.tasks/.archive/`.
155
+
156
+ ### `mrkl prune <date> [options]`
157
+
158
+ Permanently deletes archived tasks created on or before a cutoff date.
159
+
160
+ | Argument | Description |
161
+ |----------|-------------|
162
+ | `date` | Cutoff date (`YYYY-MM-DD` or `YYYYMMDD`) |
163
+
164
+ | Option | Alias | Description |
165
+ |--------|-------|-------------|
166
+ | `--force` | `-f` | Skip confirmation prompt |
167
+
168
+ Shows a confirmation prompt listing tasks to be deleted unless `--force` is used.
169
+
170
+ ```sh
171
+ # Delete archived tasks from January or earlier
172
+ mrkl prune 2026-01-31
173
+
174
+ # Skip confirmation
175
+ mrkl prune 2026-01-31 --force
176
+ ```
177
+
178
+ ### `mrkl install-skills`
179
+
180
+ Installs bundled Claude Code skills into the current project.
181
+
182
+ Copies skill directories from the mrkl package into `.claude/skills/` so they are available to Claude Code agents working in this project.
183
+
184
+ ```sh
185
+ mrkl install-skills
186
+ # 🧩 Installed plan-from-task
187
+ ```
188
+
189
+ Currently ships with:
190
+
191
+ | Skill | Description |
192
+ |-------|-------------|
193
+ | `plan-from-task` | Generate and execute implementation plans from mrkl task files |
194
+
195
+ ## Task Types 🏷️
127
196
 
128
197
  mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
129
198
 
@@ -140,7 +209,7 @@ mrkl uses [conventional commit](https://www.conventionalcommits.org/) types:
140
209
  | `build` | Build system changes |
141
210
  | `style` | Code style/formatting |
142
211
 
143
- ## Task File Format
212
+ ## Task File Format 📄
144
213
 
145
214
  Each task is a markdown file with YAML frontmatter:
146
215
 
@@ -168,7 +237,7 @@ Implement user authentication with OAuth2.
168
237
 
169
238
  The format is intentionally simple — edit task files directly when you need to update descriptions, change status, or check off criteria.
170
239
 
171
- ## Project Structure
240
+ ## Project Structure 🗂️
172
241
 
173
242
  After initialization, mrkl adds the following to your project:
174
243
 
@@ -186,7 +255,7 @@ your-project/
186
255
 
187
256
  Commit `.config/mrkl/` and `.tasks/` to version control. They're designed to be tracked alongside your code.
188
257
 
189
- ## Team Workflow
258
+ ## Team Workflow 👥
190
259
 
191
260
  When using mrkl with **git worktrees** or **protected branches**, task IDs can conflict if multiple branches create tasks concurrently. The fix is a simple convention: **separate planning from execution.**
192
261
 
@@ -210,7 +279,7 @@ mrkl done MRKL-019
210
279
 
211
280
  The counter only increments on planning branches — one at a time — so IDs never conflict. See **[docs/workflow.md](docs/workflow.md)** for the full guide with examples and edge cases.
212
281
 
213
- ## Configuration
282
+ ## Configuration ⚙️
214
283
 
215
284
  Configuration lives in `.config/mrkl/mrkl.toml` (or `mrkl.toml` at the project root):
216
285
 
@@ -224,7 +293,7 @@ tasks_dir = ".tasks"
224
293
  | `prefix` | *(required)* | Project prefix for task IDs |
225
294
  | `tasks_dir` | `".tasks"` | Directory for task files |
226
295
 
227
- ## Development
296
+ ## Development 🧑‍💻
228
297
 
229
298
  ```sh
230
299
  git clone https://github.com/xxKeefer/mrkl.git
@@ -241,10 +310,10 @@ pnpm tsx src/cli.ts list
241
310
  pnpm build
242
311
  ```
243
312
 
244
- ## Contributing
313
+ ## Contributing 🤝
245
314
 
246
315
  Contributions are welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for branch protection rules, merge strategy, and development setup.
247
316
 
248
- ## License
317
+ ## License 📜
249
318
 
250
319
  [MIT](LICENSE)
package/dist/cli.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from 'citty';
3
3
  import consola from 'consola';
4
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from 'node:fs';
5
- import { join } from 'node:path';
4
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, statSync } from 'node:fs';
5
+ import { join, dirname } from 'node:path';
6
6
  import { stringify, parse as parse$1 } from 'smol-toml';
7
7
  import matter from 'gray-matter';
8
+ import { fileURLToPath } from 'node:url';
8
9
 
9
10
  const CONFIG_PATHS = [
10
11
  join(".config", "mrkl", "mrkl.toml"),
@@ -57,7 +58,7 @@ const initCommand = defineCommand({
57
58
  const dir = process.cwd();
58
59
  try {
59
60
  initConfig(dir, { prefix: args.prefix });
60
- consola.success("mrkl initialized");
61
+ consola.success("\u{1F389} mrkl initialized");
61
62
  } catch (err) {
62
63
  consola.error(String(err.message));
63
64
  process.exit(1);
@@ -144,9 +145,7 @@ function createTask(opts) {
144
145
  function listTasks(filter) {
145
146
  const config = loadConfig(filter.dir);
146
147
  const tasksDir = join(filter.dir, config.tasks_dir);
147
- const files = readdirSync(tasksDir).filter(
148
- (f) => f.endsWith(".md") && !f.startsWith(".")
149
- );
148
+ const files = readdirSync(tasksDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
150
149
  let tasks = files.flatMap((f) => {
151
150
  try {
152
151
  const content = readFileSync(join(tasksDir, f), "utf-8");
@@ -164,8 +163,9 @@ function listTasks(filter) {
164
163
  function archiveTask(dir, id) {
165
164
  const config = loadConfig(dir);
166
165
  const tasksDir = join(dir, config.tasks_dir);
166
+ const idUpper = id.toUpperCase();
167
167
  const file = readdirSync(tasksDir).find(
168
- (f) => f.endsWith(".md") && f.startsWith(id)
168
+ (f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
169
169
  );
170
170
  if (!file) {
171
171
  throw new Error(`Task ${id} not found`);
@@ -178,6 +178,70 @@ function archiveTask(dir, id) {
178
178
  writeFileSync(archivePath, render(task));
179
179
  unlinkSync(filePath);
180
180
  }
181
+ function parseCutoffDate(input) {
182
+ const normalized = input.replace(/^(\d{4})(\d{2})(\d{2})$/, "$1-$2-$3");
183
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
184
+ throw new Error(`Invalid date format: "${input}". Expected YYYY-MM-DD or YYYYMMDD.`);
185
+ }
186
+ const [y, m, d] = normalized.split("-").map(Number);
187
+ const date = new Date(y, m - 1, d);
188
+ if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
189
+ throw new Error(`Invalid date: "${input}" is not a real calendar date.`);
190
+ }
191
+ return normalized;
192
+ }
193
+ function normalizeCreatedDate(created) {
194
+ if (created instanceof Date) {
195
+ return created.toISOString().slice(0, 10);
196
+ }
197
+ return String(created);
198
+ }
199
+ function pruneTasks(dir, cutoff) {
200
+ const config = loadConfig(dir);
201
+ const archiveDir = join(dir, config.tasks_dir, ".archive");
202
+ if (!existsSync(archiveDir)) {
203
+ return { deleted: [], total: 0 };
204
+ }
205
+ const files = readdirSync(archiveDir).filter((f) => f.endsWith(".md") && !f.startsWith("."));
206
+ const deleted = [];
207
+ for (const f of files) {
208
+ try {
209
+ const content = readFileSync(join(archiveDir, f), "utf-8");
210
+ const task = parse(content, f);
211
+ const created = normalizeCreatedDate(task.created);
212
+ if (created <= cutoff) {
213
+ deleted.push({ id: task.id, title: task.title, created, filename: f });
214
+ }
215
+ } catch {
216
+ }
217
+ }
218
+ return { deleted, total: files.length };
219
+ }
220
+ function executePrune(dir, filenames) {
221
+ const config = loadConfig(dir);
222
+ const archiveDir = join(dir, config.tasks_dir, ".archive");
223
+ for (const f of filenames) {
224
+ unlinkSync(join(archiveDir, f));
225
+ }
226
+ }
227
+ function closeTask(dir, id) {
228
+ const config = loadConfig(dir);
229
+ const tasksDir = join(dir, config.tasks_dir);
230
+ const idUpper = id.toUpperCase();
231
+ const file = readdirSync(tasksDir).find(
232
+ (f) => f.endsWith(".md") && f.toUpperCase().startsWith(idUpper)
233
+ );
234
+ if (!file) {
235
+ throw new Error(`Task ${id} not found`);
236
+ }
237
+ const filePath = join(tasksDir, file);
238
+ const content = readFileSync(filePath, "utf-8");
239
+ const task = parse(content, file);
240
+ task.status = "closed";
241
+ const archivePath = join(tasksDir, ".archive", file);
242
+ writeFileSync(archivePath, render(task));
243
+ unlinkSync(filePath);
244
+ }
181
245
 
182
246
  const TASK_TYPES = [
183
247
  "feat",
@@ -192,6 +256,43 @@ const TASK_TYPES = [
192
256
  "style"
193
257
  ];
194
258
 
259
+ async function promptForTask(dir) {
260
+ const type = await consola.prompt("Task type", {
261
+ type: "select",
262
+ options: TASK_TYPES.map((t) => t)
263
+ });
264
+ if (typeof type === "symbol") process.exit(0);
265
+ const title = await consola.prompt("Task title", {
266
+ type: "text",
267
+ placeholder: "e.g. add user authentication"
268
+ });
269
+ if (typeof title === "symbol") process.exit(0);
270
+ if (!title.trim()) {
271
+ consola.error("\u274C Title cannot be empty");
272
+ process.exit(1);
273
+ }
274
+ const desc = await consola.prompt("Description (optional, enter to skip)", {
275
+ type: "text",
276
+ placeholder: "Describe the task in detail"
277
+ });
278
+ if (typeof desc === "symbol") process.exit(0);
279
+ const criteria = [];
280
+ while (true) {
281
+ const ac = await consola.prompt(
282
+ criteria.length === 0 ? "Acceptance criterion (Esc to skip)" : `Criterion #${criteria.length + 1} (Esc to finish)`,
283
+ { type: "text" }
284
+ );
285
+ if (typeof ac === "symbol") break;
286
+ if (ac.trim()) criteria.push(ac.trim());
287
+ }
288
+ return {
289
+ dir,
290
+ type,
291
+ title,
292
+ description: desc || void 0,
293
+ acceptance_criteria: criteria.length > 0 ? criteria : void 0
294
+ };
295
+ }
195
296
  const createCommand = defineCommand({
196
297
  meta: {
197
298
  name: "create",
@@ -201,37 +302,47 @@ const createCommand = defineCommand({
201
302
  type: {
202
303
  type: "positional",
203
304
  description: "Task type (feat, fix, chore, docs, perf, refactor, test, ci, build, style)",
204
- required: true
305
+ required: false
205
306
  },
206
307
  title: {
207
308
  type: "positional",
208
309
  description: "Task title",
209
- required: true
310
+ required: false
210
311
  },
211
312
  desc: {
212
313
  type: "string",
314
+ alias: "d",
213
315
  description: "Task description"
214
316
  },
215
317
  ac: {
216
318
  type: "string",
319
+ alias: "a",
217
320
  description: "Acceptance criterion (can be specified multiple times)"
218
321
  }
219
322
  },
220
- run({ args }) {
221
- if (!TASK_TYPES.includes(args.type)) {
222
- consola.error(`Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
323
+ async run({ args }) {
324
+ const dir = process.cwd();
325
+ const interactive = !args.type && !args.title;
326
+ if (!interactive && (!args.type || !args.title)) {
327
+ consola.error("\u274C Both type and title are required, or omit both for interactive mode");
223
328
  process.exit(1);
224
329
  }
225
- const dir = process.cwd();
226
330
  try {
227
- const task = createTask({
331
+ const opts = interactive ? await promptForTask(dir) : {
228
332
  dir,
229
- type: args.type,
333
+ type: (() => {
334
+ if (!TASK_TYPES.includes(args.type)) {
335
+ consola.error(`\u274C Invalid type "${args.type}". Must be one of: ${TASK_TYPES.join(", ")}`);
336
+ process.exit(1);
337
+ }
338
+ return args.type;
339
+ })(),
230
340
  title: args.title,
231
341
  description: args.desc,
232
342
  acceptance_criteria: args.ac ? Array.isArray(args.ac) ? args.ac : [args.ac] : void 0
233
- });
234
- consola.success(`Created ${task.id}: ${task.title}`);
343
+ };
344
+ const task = createTask(opts);
345
+ consola.success(`\u{1F4DD} Created ${task.id}: ${task.title}`);
235
346
  } catch (err) {
236
347
  consola.error(String(err.message));
237
348
  process.exit(1);
@@ -247,10 +358,12 @@ const listCommand = defineCommand({
247
358
  args: {
248
359
  type: {
249
360
  type: "string",
361
+ alias: "t",
250
362
  description: "Filter by task type"
251
363
  },
252
364
  status: {
253
365
  type: "string",
366
+ alias: "s",
254
367
  description: "Filter by status (todo, in-progress, done)"
255
368
  }
256
369
  },
@@ -263,7 +376,7 @@ const listCommand = defineCommand({
263
376
  status: args.status
264
377
  });
265
378
  if (tasks.length === 0) {
266
- consola.info("No tasks found");
379
+ consola.info("\u{1F4ED} No tasks found");
267
380
  return;
268
381
  }
269
382
  for (const task of tasks) {
@@ -292,7 +405,138 @@ const doneCommand = defineCommand({
292
405
  const dir = process.cwd();
293
406
  try {
294
407
  archiveTask(dir, args.id);
295
- consola.success(`Archived ${args.id}`);
408
+ consola.success(`\u2705 Archived ${args.id}`);
409
+ } catch (err) {
410
+ consola.error(String(err.message));
411
+ process.exit(1);
412
+ }
413
+ }
414
+ });
415
+
416
+ const pruneCommand = defineCommand({
417
+ meta: {
418
+ name: "prune",
419
+ description: "Delete archived tasks created on or before a given date"
420
+ },
421
+ args: {
422
+ date: {
423
+ type: "positional",
424
+ description: "Cutoff date (YYYY-MM-DD or YYYYMMDD)",
425
+ required: true
426
+ },
427
+ force: {
428
+ type: "boolean",
429
+ alias: "f",
430
+ description: "Skip confirmation prompt",
431
+ default: false
432
+ }
433
+ },
434
+ async run({ args }) {
435
+ const dir = process.cwd();
436
+ let cutoff;
437
+ try {
438
+ cutoff = parseCutoffDate(args.date);
439
+ } catch (err) {
440
+ consola.error(String(err.message));
441
+ process.exit(1);
442
+ }
443
+ const result = pruneTasks(dir, cutoff);
444
+ if (result.deleted.length === 0) {
445
+ consola.info(`\u{1F4ED} No archived tasks found on or before ${cutoff}`);
446
+ return;
447
+ }
448
+ consola.info(`\u{1F50D} Found ${result.deleted.length} task(s) to prune:`);
449
+ for (const task of result.deleted) {
450
+ consola.log(` ${task.id} \u2014 ${task.title} (${task.created})`);
451
+ }
452
+ if (!args.force) {
453
+ const confirm = await consola.prompt("Delete these tasks?", {
454
+ type: "confirm"
455
+ });
456
+ if (typeof confirm === "symbol" || !confirm) {
457
+ consola.info("\u{1F44B} Aborted");
458
+ return;
459
+ }
460
+ }
461
+ executePrune(dir, result.deleted.map((t) => t.filename));
462
+ consola.success(`\u{1F9F9} Pruned ${result.deleted.length} archived task(s)`);
463
+ }
464
+ });
465
+
466
+ const closeCommand = defineCommand({
467
+ meta: {
468
+ name: "close",
469
+ description: "Close a task (won't do, duplicate, etc.) and archive it"
470
+ },
471
+ args: {
472
+ id: {
473
+ type: "positional",
474
+ description: "Task ID to close (e.g., VON-001)",
475
+ required: true
476
+ }
477
+ },
478
+ run({ args }) {
479
+ const dir = process.cwd();
480
+ try {
481
+ closeTask(dir, args.id);
482
+ consola.success(`\u{1F6AB} Closed ${args.id}`);
483
+ } catch (err) {
484
+ consola.error(String(err.message));
485
+ process.exit(1);
486
+ }
487
+ }
488
+ });
489
+
490
+ function findPackageRoot() {
491
+ let dir = dirname(fileURLToPath(import.meta.url));
492
+ while (dir !== dirname(dir)) {
493
+ if (existsSync(join(dir, "package.json"))) {
494
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
495
+ if (pkg.name === "@xxkeefer/mrkl") return dir;
496
+ }
497
+ dir = dirname(dir);
498
+ }
499
+ throw new Error("Could not locate mrkl package root");
500
+ }
501
+ function copyDirSync(src, dest) {
502
+ mkdirSync(dest, { recursive: true });
503
+ for (const entry of readdirSync(src)) {
504
+ const srcPath = join(src, entry);
505
+ const destPath = join(dest, entry);
506
+ if (statSync(srcPath).isDirectory()) {
507
+ copyDirSync(srcPath, destPath);
508
+ } else {
509
+ writeFileSync(destPath, readFileSync(srcPath));
510
+ }
511
+ }
512
+ }
513
+ const installSkillsCommand = defineCommand({
514
+ meta: {
515
+ name: "install-skills",
516
+ description: "Install mrkl Claude Code skills into the current project"
517
+ },
518
+ run() {
519
+ const dest = join(process.cwd(), ".claude", "skills");
520
+ try {
521
+ const packageRoot = findPackageRoot();
522
+ const skillsDir = join(packageRoot, "skills");
523
+ if (!existsSync(skillsDir)) {
524
+ consola.error("\u274C No skills directory found in mrkl package");
525
+ process.exit(1);
526
+ }
527
+ const skills = readdirSync(skillsDir).filter(
528
+ (f) => statSync(join(skillsDir, f)).isDirectory()
529
+ );
530
+ if (skills.length === 0) {
531
+ consola.info("\u{1F4ED} No skills to install");
532
+ return;
533
+ }
534
+ for (const skill of skills) {
535
+ const src = join(skillsDir, skill);
536
+ const target = join(dest, skill);
537
+ copyDirSync(src, target);
538
+ consola.success(`\u{1F9E9} Installed ${skill}`);
539
+ }
296
540
  } catch (err) {
297
541
  consola.error(String(err.message));
298
542
  process.exit(1);
@@ -308,10 +552,18 @@ const main = defineCommand({
308
552
  },
309
553
  subCommands: {
310
554
  init: initCommand,
555
+ i: initCommand,
311
556
  create: createCommand,
312
557
  c: createCommand,
313
558
  list: listCommand,
314
- done: doneCommand
559
+ ls: listCommand,
560
+ done: doneCommand,
561
+ d: doneCommand,
562
+ prune: pruneCommand,
563
+ p: pruneCommand,
564
+ close: closeCommand,
565
+ x: closeCommand,
566
+ "install-skills": installSkillsCommand
315
567
  }
316
568
  });
317
569
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xxkeefer/mrkl",
3
- "version": "0.2.14",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Lightweight CLI tool for structured markdown task tracking",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "license": "MIT",
16
16
  "files": [
17
- "dist"
17
+ "dist",
18
+ "skills"
18
19
  ],
19
20
  "repository": {
20
21
  "type": "git",
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: plan-from-task
3
+ description: Generate and execute implementation plans from mrkl task files (.tasks/ directory). Use when user references a task ID (e.g., MRKL-001, PROJ-042) or a @path to a .tasks/ markdown file and wants to plan or implement it.
4
+ ---
5
+
6
+ # Plan From Task
7
+
8
+ Create implementation plans from mrkl task files and execute them.
9
+
10
+ ## Quick start
11
+
12
+ The user provides a task reference in one of two forms:
13
+
14
+ - **File path**: `@".tasks/PROJ-001 feat - user auth.md"` or `implement .tasks/PROJ-001...`
15
+ - **Task ID**: `PROJ-001` or `mrkl-042` (case-insensitive)
16
+
17
+ ## Workflow
18
+
19
+ ### 1. Locate and read the task
20
+
21
+ - **File path given**: read the file directly
22
+ - **Task ID given**: glob for `.tasks/<ID>*.md` (case-insensitive) and read the match
23
+
24
+ Parse the YAML frontmatter and markdown body. Task files follow this structure:
25
+
26
+ ```markdown
27
+ ---
28
+ id: PROJ-001
29
+ type: feat
30
+ status: todo
31
+ created: '2026-03-01'
32
+ ---
33
+
34
+ ## Description
35
+
36
+ [What needs to be done]
37
+
38
+ ## Acceptance Criteria
39
+
40
+ - [ ] first criterion
41
+ - [ ] second criterion
42
+ ```
43
+
44
+ ### 2. Assess completeness
45
+
46
+ If the description or acceptance criteria are too vague to plan from:
47
+
48
+ - Ask the user targeted clarifying questions about scope, approach, or constraints
49
+ - Gather enough context to produce a concrete implementation plan
50
+
51
+ ### 3. Plan the implementation
52
+
53
+ 1. Enter plan mode
54
+ 2. Explore the codebase to understand relevant architecture and patterns
55
+ 3. Write a step-by-step implementation plan that addresses every acceptance criterion
56
+ 4. Present the plan for user approval
57
+
58
+ ### 4. Execute the plan
59
+
60
+ After approval, implement in this order:
61
+
62
+ 1. **Update the task file first** — rewrite the `## Description` body and `## Acceptance Criteria`
63
+ checklist to reflect the refined, concrete plan of what will be delivered.
64
+ The task file doubles as the PR description, so make it clear and accurate.
65
+ 2. Then implement the code changes following the plan.
66
+
67
+ ## Rules
68
+
69
+ - **NEVER modify YAML frontmatter** — `id`, `type`, `status`, `created` are immutable
70
+ - **NEVER modify markdown headings** — `## Description` and `## Acceptance Criteria` must stay as-is
71
+ - You MAY rewrite the content under those headings to match the approved plan
72
+ - Use `- [ ]` checkbox format for acceptance criteria
73
+ - Follow existing codebase conventions discovered during exploration