@joshski/dust 0.1.7 → 0.1.8

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,23 +1,25 @@
1
1
  # Dust
2
2
 
3
- A lightweight planning system and work tracker optimised for humans working with AI agents.
3
+ A lightweight workflow tool for humans working with AI agents.
4
4
 
5
5
  [![CI](https://github.com/joshski/dust/actions/workflows/ci.yml/badge.svg)](https://github.com/joshski/dust/actions/workflows/ci.yml)
6
6
 
7
+ ## Why Would I Use This?
8
+
9
+ Use this to plan a series of tasks that coding agents can perform autonomously.
10
+
7
11
  ## Getting Started
8
12
 
9
- Install dust:
13
+ Install dust using your package manager of choice (`npm` and `bun` officially supported for now):
10
14
 
11
15
  ```bash
12
- npm install -g @joshski/dust
16
+ npm install @joshski/dust
13
17
  ```
14
18
 
15
19
  Initialize dust in your repository:
16
20
 
17
21
  ```bash
18
22
  npx dust init
19
- # or
20
- bunx dust init
21
23
  ```
22
24
 
23
25
  ## How It Works
@@ -39,3 +41,54 @@ These files are used to facilitate exploration and management of AI agent workfl
39
41
  Progress is tracked via changes to markdown files in the `.dust/` directory. The four directories together (`goals/`, `ideas/`, `tasks/`, `facts/`) act as a kanban system for managing work.
40
42
 
41
43
  The `tasks/` directory acts as a work queue. When a task is completed, the commit typically includes both the code changes and the deletion of the task file—removing work from the queue for subsequent agents.
44
+
45
+ Humans primarily interact with `dust` using their AI coding agent of choice, e.g.
46
+
47
+ ```
48
+ > claude "implement the next task"
49
+ > claude "get to work!"
50
+ > codex "add task: coverage reporting"
51
+ > codex "goal: cross-platform compatibility"
52
+ ```
53
+
54
+ Agents discover everything they need to know to interpret human prompts using the `dust` CLI.
55
+
56
+ ## Agent CLI
57
+
58
+ The `dust` CLI is intended to be used primarily by agents, not by humans (except for the `init` command, as mentioned above).
59
+
60
+ Don't let that stop you:
61
+
62
+ ```bash
63
+ npx dust help
64
+ ```
65
+
66
+ ## AGENTS.md
67
+
68
+ Add a line like this to your `AGENTS.md` or `CLAUDE.md` file:
69
+
70
+ ```markdown
71
+ Always immediately run `npx dust agent` when you start working in this repository.
72
+ ```
73
+
74
+ In fact, you might find it effective to use that as the _only_ line in those files. Essential documentation should be discoverable by agents exploring your `./.dust` directory.
75
+
76
+ ## Configuration
77
+
78
+ Configure hooks into your tools in `./.dust/config/settings.json` e.g.
79
+
80
+ ```json
81
+ {
82
+ "dustCommand": "bunx dust",
83
+ "checks": [
84
+ { "name": "lint", "command": "bunx biome check ." },
85
+ { "name": "build", "command": "bun run build" },
86
+ { "name": "tests", "command": "bun run test:coverage" },
87
+ { "name": "typecheck", "command": "bunx tsc --noEmit lib/**/*.ts" }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ The `dust check` command will run all of the configured checks in parallel and produce a very terse (context window-friendly) output unless one or more of the checks fail.
93
+
94
+ Agents are instructed to run `dust check` before and after any changes, as a way of keeping them on track. It's more important that these commands are comprehensive, than they are fast.
package/dist/dust.js CHANGED
@@ -18,7 +18,7 @@ function loadTemplate(name, variables = {}) {
18
18
  return content;
19
19
  }
20
20
 
21
- // lib/cli/agent.ts
21
+ // lib/cli/commands/agent.ts
22
22
  var AGENT_SUBCOMMANDS = [
23
23
  "work",
24
24
  "tasks",
@@ -26,45 +26,32 @@ var AGENT_SUBCOMMANDS = [
26
26
  "ideas",
27
27
  "help"
28
28
  ];
29
- function generateAgentGreeting(settings) {
30
- return loadTemplate("agent-greeting", { bin: settings.dustCommand });
29
+ function templateVariables(settings) {
30
+ return { bin: settings.dustCommand };
31
31
  }
32
- function generateWorkInstructions(settings) {
33
- return loadTemplate("agent-work", { bin: settings.dustCommand });
34
- }
35
- function generateTasksInstructions(settings) {
36
- return loadTemplate("agent-tasks", { bin: settings.dustCommand });
37
- }
38
- function generateGoalsInstructions(settings) {
39
- return loadTemplate("agent-goals", { bin: settings.dustCommand });
40
- }
41
- function generateIdeasInstructions(settings) {
42
- return loadTemplate("agent-ideas", { bin: settings.dustCommand });
43
- }
44
- function generateAgentHelp(settings) {
45
- return loadTemplate("agent-help", { bin: settings.dustCommand });
46
- }
47
- async function agent(ctx, args, settings) {
32
+ async function agent(deps) {
33
+ const { arguments: args, context: ctx, settings } = deps;
48
34
  const subcommand = args[0];
35
+ const vars = templateVariables(settings);
49
36
  if (!subcommand) {
50
- ctx.stdout(generateAgentGreeting(settings));
37
+ ctx.stdout(loadTemplate("agent-greeting", vars));
51
38
  return { exitCode: 0 };
52
39
  }
53
40
  switch (subcommand) {
54
41
  case "work":
55
- ctx.stdout(generateWorkInstructions(settings));
42
+ ctx.stdout(loadTemplate("agent-work", vars));
56
43
  return { exitCode: 0 };
57
44
  case "tasks":
58
- ctx.stdout(generateTasksInstructions(settings));
45
+ ctx.stdout(loadTemplate("agent-tasks", vars));
59
46
  return { exitCode: 0 };
60
47
  case "goals":
61
- ctx.stdout(generateGoalsInstructions(settings));
48
+ ctx.stdout(loadTemplate("agent-goals", vars));
62
49
  return { exitCode: 0 };
63
50
  case "ideas":
64
- ctx.stdout(generateIdeasInstructions(settings));
51
+ ctx.stdout(loadTemplate("agent-ideas", vars));
65
52
  return { exitCode: 0 };
66
53
  case "help":
67
- ctx.stdout(generateAgentHelp(settings));
54
+ ctx.stdout(loadTemplate("agent-help", vars));
68
55
  return { exitCode: 0 };
69
56
  default:
70
57
  ctx.stderr(`Unknown subcommand: ${subcommand}`);
@@ -73,59 +60,20 @@ async function agent(ctx, args, settings) {
73
60
  }
74
61
  }
75
62
 
76
- // lib/cli/check.ts
63
+ // lib/cli/commands/check.ts
77
64
  import { spawn } from "node:child_process";
78
65
 
79
- // lib/cli/settings.ts
80
- import { join as join2 } from "node:path";
81
- var DEFAULT_SETTINGS = {
82
- dustCommand: "npx dust"
83
- };
84
- function detectDustCommand(cwd, fs) {
85
- if (fs.exists(join2(cwd, "bun.lockb"))) {
86
- return "bunx dust";
87
- }
88
- if (fs.exists(join2(cwd, "pnpm-lock.yaml"))) {
89
- return "pnpx dust";
90
- }
91
- if (fs.exists(join2(cwd, "package-lock.json"))) {
92
- return "npx dust";
93
- }
94
- if (process.env.BUN_INSTALL) {
95
- return "bunx dust";
96
- }
97
- return "npx dust";
98
- }
99
- async function loadSettings(cwd, fs) {
100
- const settingsPath = join2(cwd, ".dust", "config", "settings.json");
101
- if (!fs.exists(settingsPath)) {
102
- return {
103
- dustCommand: detectDustCommand(cwd, fs)
104
- };
105
- }
106
- try {
107
- const content = await fs.readFile(settingsPath);
108
- const parsed = JSON.parse(content);
109
- if (!parsed.dustCommand) {
110
- return {
111
- ...DEFAULT_SETTINGS,
112
- ...parsed,
113
- dustCommand: detectDustCommand(cwd, fs)
114
- };
115
- }
116
- return {
117
- ...DEFAULT_SETTINGS,
118
- ...parsed
119
- };
120
- } catch {
121
- return {
122
- dustCommand: detectDustCommand(cwd, fs)
123
- };
124
- }
66
+ // lib/cli/commands/validate.ts
67
+ import { dirname as dirname2, resolve } from "node:path";
68
+
69
+ // lib/cli/markdown-utilities.ts
70
+ function extractTitle(content) {
71
+ const match = content.match(/^#\s+(.+)$/m);
72
+ return match ? match[1].trim() : null;
125
73
  }
74
+ var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
126
75
 
127
- // lib/cli/validate.ts
128
- import { dirname as dirname2, resolve } from "node:path";
76
+ // lib/cli/commands/validate.ts
129
77
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked by", "## Definition of done"];
130
78
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
131
79
  function validateFilename(filePath) {
@@ -158,7 +106,7 @@ function validateLinks(filePath, content, fs) {
158
106
  const fileDir = dirname2(filePath);
159
107
  for (let i = 0;i < lines.length; i++) {
160
108
  const line = lines[i];
161
- const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
109
+ const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
162
110
  let match = linkPattern.exec(line);
163
111
  while (match) {
164
112
  const linkTarget = match[2];
@@ -205,7 +153,7 @@ function validateSemanticLinks(filePath, content) {
205
153
  const rule = SEMANTIC_RULES.find((r) => r.section === currentSection);
206
154
  if (!rule)
207
155
  continue;
208
- const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
156
+ const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
209
157
  let match = linkPattern.exec(line);
210
158
  while (match) {
211
159
  const linkTarget = match[2];
@@ -241,7 +189,8 @@ function validateSemanticLinks(filePath, content) {
241
189
  }
242
190
  return violations;
243
191
  }
244
- async function validate(ctx, fs, _args, glob) {
192
+ async function validate(deps) {
193
+ const { context: ctx, fileSystem: fs, globScanner: glob } = deps;
245
194
  const dustPath = `${ctx.cwd}/.dust`;
246
195
  if (!fs.exists(dustPath)) {
247
196
  ctx.stderr("Error: .dust directory not found");
@@ -287,7 +236,7 @@ async function validate(ctx, fs, _args, glob) {
287
236
  return { exitCode: 1 };
288
237
  }
289
238
 
290
- // lib/cli/check.ts
239
+ // lib/cli/commands/check.ts
291
240
  function createBufferedRunner(spawnFn) {
292
241
  return {
293
242
  run: (command, cwd) => {
@@ -323,14 +272,18 @@ async function runConfiguredChecks(checks, cwd, runner) {
323
272
  });
324
273
  return Promise.all(promises);
325
274
  }
326
- async function runValidationCheck(ctx, fs, glob) {
275
+ async function runValidationCheck(deps) {
327
276
  const outputLines = [];
328
277
  const bufferedCtx = {
329
- cwd: ctx.cwd,
278
+ cwd: deps.context.cwd,
330
279
  stdout: (msg) => outputLines.push(msg),
331
280
  stderr: (msg) => outputLines.push(msg)
332
281
  };
333
- const result = await validate(bufferedCtx, fs, [], glob);
282
+ const result = await validate({
283
+ ...deps,
284
+ context: bufferedCtx,
285
+ arguments: []
286
+ });
334
287
  return {
335
288
  name: "validate",
336
289
  command: "dust validate",
@@ -361,8 +314,8 @@ function displayResults(results, ctx) {
361
314
  ctx.stdout(`${passed.length}/${results.length} checks passed`);
362
315
  return failed.length > 0 ? 1 : 0;
363
316
  }
364
- async function check(ctx, fs, _args, glob, bufferedRunner = defaultBufferedRunner) {
365
- const settings = await loadSettings(ctx.cwd, fs);
317
+ async function check(deps, bufferedRunner = defaultBufferedRunner) {
318
+ const { context: ctx, fileSystem: fs, settings } = deps;
366
319
  if (!settings.checks || settings.checks.length === 0) {
367
320
  ctx.stderr("Error: No checks configured in .dust/config/settings.json");
368
321
  ctx.stderr("");
@@ -376,8 +329,9 @@ async function check(ctx, fs, _args, glob, bufferedRunner = defaultBufferedRunne
376
329
  return { exitCode: 1 };
377
330
  }
378
331
  const checkPromises = [];
379
- if (glob) {
380
- checkPromises.push(runValidationCheck(ctx, fs, glob));
332
+ const dustPath = `${ctx.cwd}/.dust`;
333
+ if (fs.exists(dustPath)) {
334
+ checkPromises.push(runValidationCheck(deps));
381
335
  }
382
336
  checkPromises.push(runConfiguredChecks(settings.checks, ctx.cwd, bufferedRunner));
383
337
  const promiseResults = await Promise.all(checkPromises);
@@ -393,13 +347,74 @@ async function check(ctx, fs, _args, glob, bufferedRunner = defaultBufferedRunne
393
347
  return { exitCode };
394
348
  }
395
349
 
396
- // lib/cli/init.ts
397
- var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts"];
350
+ // lib/cli/settings.ts
351
+ import { join as join2 } from "node:path";
352
+ var DEFAULT_SETTINGS = {
353
+ dustCommand: "npx dust"
354
+ };
355
+ function detectDustCommand(cwd, fs) {
356
+ if (fs.exists(join2(cwd, "bun.lockb"))) {
357
+ return "bunx dust";
358
+ }
359
+ if (fs.exists(join2(cwd, "pnpm-lock.yaml"))) {
360
+ return "pnpx dust";
361
+ }
362
+ if (fs.exists(join2(cwd, "package-lock.json"))) {
363
+ return "npx dust";
364
+ }
365
+ if (process.env.BUN_INSTALL) {
366
+ return "bunx dust";
367
+ }
368
+ return "npx dust";
369
+ }
370
+ async function loadSettings(cwd, fs) {
371
+ const settingsPath = join2(cwd, ".dust", "config", "settings.json");
372
+ if (!fs.exists(settingsPath)) {
373
+ return {
374
+ dustCommand: detectDustCommand(cwd, fs)
375
+ };
376
+ }
377
+ try {
378
+ const content = await fs.readFile(settingsPath);
379
+ const parsed = JSON.parse(content);
380
+ if (!parsed.dustCommand) {
381
+ return {
382
+ ...DEFAULT_SETTINGS,
383
+ ...parsed,
384
+ dustCommand: detectDustCommand(cwd, fs)
385
+ };
386
+ }
387
+ return {
388
+ ...DEFAULT_SETTINGS,
389
+ ...parsed
390
+ };
391
+ } catch {
392
+ return {
393
+ dustCommand: detectDustCommand(cwd, fs)
394
+ };
395
+ }
396
+ }
397
+
398
+ // lib/cli/commands/init.ts
399
+ var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
400
+ function generateSettings(cwd, fs) {
401
+ const dustCommand = detectDustCommand(cwd, fs);
402
+ const checks = [];
403
+ if (fs.exists(`${cwd}/bun.lockb`)) {
404
+ checks.push({ name: "test", command: "bun test" });
405
+ } else if (fs.exists(`${cwd}/pnpm-lock.yaml`)) {
406
+ checks.push({ name: "test", command: "pnpm test" });
407
+ } else if (fs.exists(`${cwd}/package-lock.json`) || fs.exists(`${cwd}/package.json`)) {
408
+ checks.push({ name: "test", command: "npm test" });
409
+ }
410
+ return { dustCommand, checks };
411
+ }
398
412
  var USE_DUST_FACT = `# Use dust for planning
399
413
 
400
414
  This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
401
415
  `;
402
- async function init(ctx, fs, _args) {
416
+ async function init(deps) {
417
+ const { context: ctx, fileSystem: fs } = deps;
403
418
  const dustPath = `${ctx.cwd}/.dust`;
404
419
  const dustCommand = detectDustCommand(ctx.cwd, fs);
405
420
  const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
@@ -411,9 +426,13 @@ async function init(ctx, fs, _args) {
411
426
  await fs.mkdir(`${dustPath}/${dir}`, { recursive: true });
412
427
  }
413
428
  await fs.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT);
429
+ const settings = generateSettings(ctx.cwd, fs);
430
+ await fs.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
431
+ `);
414
432
  ctx.stdout("Initialized Dust repository in .dust/");
415
433
  ctx.stdout(`Created directories: ${DUST_DIRECTORIES.join(", ")}`);
416
434
  ctx.stdout("Created initial fact: .dust/facts/use-dust-for-planning.md");
435
+ ctx.stdout("Created settings: .dust/config/settings.json");
417
436
  }
418
437
  const claudeMdPath = `${ctx.cwd}/CLAUDE.md`;
419
438
  if (fs.exists(claudeMdPath)) {
@@ -431,16 +450,23 @@ async function init(ctx, fs, _args) {
431
450
  await fs.writeFile(agentsMdPath, agentsContent);
432
451
  ctx.stdout("Created AGENTS.md with agent instructions");
433
452
  }
453
+ const runner = dustCommand.split(" ")[0];
454
+ ctx.stdout("");
455
+ ctx.stdout("Commit the changes if you are happy, then get planning!");
456
+ ctx.stdout("");
457
+ ctx.stdout("If this is a new repository, you can start adding ideas or tasks right away:");
458
+ ctx.stdout(`> ${runner} claude "Idea: friendly UI for non-technical users"`);
459
+ ctx.stdout(`> ${runner} codex "Task: set up code coverage"`);
460
+ ctx.stdout("");
461
+ ctx.stdout("If this is an existing codebase, you might want to backfill goals and facts:");
462
+ ctx.stdout(`> ${runner} claude "Add goals and facts based on the code in this repository"`);
434
463
  return { exitCode: 0 };
435
464
  }
436
465
 
437
- // lib/cli/list.ts
466
+ // lib/cli/commands/list.ts
438
467
  var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
439
- function extractTitle(content) {
440
- const match = content.match(/^#\s+(.+)$/m);
441
- return match ? match[1].trim() : null;
442
- }
443
- async function list(ctx, fs, args) {
468
+ async function list(deps) {
469
+ const { arguments: args, context: ctx, fileSystem: fs } = deps;
444
470
  const dustPath = `${ctx.cwd}/.dust`;
445
471
  if (!fs.exists(dustPath)) {
446
472
  ctx.stderr("Error: .dust directory not found");
@@ -480,11 +506,7 @@ async function list(ctx, fs, args) {
480
506
  return { exitCode: 0 };
481
507
  }
482
508
 
483
- // lib/cli/next.ts
484
- function extractTitle2(content) {
485
- const match = content.match(/^#\s+(.+)$/m);
486
- return match ? match[1].trim() : null;
487
- }
509
+ // lib/cli/commands/next.ts
488
510
  function extractBlockedBy(content) {
489
511
  const blockedByMatch = content.match(/^## Blocked by\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
490
512
  if (!blockedByMatch) {
@@ -503,7 +525,8 @@ function extractBlockedBy(content) {
503
525
  }
504
526
  return blockers;
505
527
  }
506
- async function next(ctx, fs, _args) {
528
+ async function next(deps) {
529
+ const { context: ctx, fileSystem: fs } = deps;
507
530
  const dustPath = `${ctx.cwd}/.dust`;
508
531
  if (!fs.exists(dustPath)) {
509
532
  ctx.stderr("Error: .dust directory not found");
@@ -527,9 +550,9 @@ async function next(ctx, fs, _args) {
527
550
  const blockers = extractBlockedBy(content);
528
551
  const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
529
552
  if (!hasIncompleteBlocker) {
530
- const title = extractTitle2(content);
531
- const name = file.replace(/\.md$/, "");
532
- unblockedTasks.push({ name, title });
553
+ const title = extractTitle(content);
554
+ const relativePath = `.dust/tasks/${file}`;
555
+ unblockedTasks.push({ path: relativePath, title });
533
556
  }
534
557
  }
535
558
  if (unblockedTasks.length === 0) {
@@ -538,9 +561,9 @@ async function next(ctx, fs, _args) {
538
561
  ctx.stdout("Next tasks:");
539
562
  for (const task of unblockedTasks) {
540
563
  if (task.title) {
541
- ctx.stdout(` ${task.name} - ${task.title}`);
564
+ ctx.stdout(` ${task.path} - ${task.title}`);
542
565
  } else {
543
- ctx.stdout(` ${task.name}`);
566
+ ctx.stdout(` ${task.path}`);
544
567
  }
545
568
  }
546
569
  return { exitCode: 0 };
@@ -566,22 +589,22 @@ function isHelpRequest(command) {
566
589
  function isValidCommand(command) {
567
590
  return COMMANDS.includes(command);
568
591
  }
569
- async function runCommand(command, commandArgs, ctx, fs, glob, settings) {
592
+ async function runCommand(command, deps) {
570
593
  switch (command) {
571
594
  case "init":
572
- return init(ctx, fs, commandArgs);
595
+ return init(deps);
573
596
  case "validate":
574
- return validate(ctx, fs, commandArgs, glob);
597
+ return validate(deps);
575
598
  case "list":
576
- return list(ctx, fs, commandArgs);
599
+ return list(deps);
577
600
  case "next":
578
- return next(ctx, fs, commandArgs);
601
+ return next(deps);
579
602
  case "check":
580
- return check(ctx, fs, commandArgs, glob);
603
+ return check(deps);
581
604
  case "agent":
582
- return agent(ctx, commandArgs, settings);
605
+ return agent(deps);
583
606
  case "help":
584
- ctx.stdout(generateHelpText(settings));
607
+ deps.context.stdout(generateHelpText(deps.settings));
585
608
  return { exitCode: 0 };
586
609
  }
587
610
  }
@@ -600,7 +623,14 @@ async function main(options) {
600
623
  ctx.stderr(`Run '${settings.dustCommand} help' for available commands`);
601
624
  return { exitCode: 1 };
602
625
  }
603
- return runCommand(command, commandArgs, ctx, fs, glob, settings);
626
+ const deps = {
627
+ arguments: commandArgs,
628
+ context: ctx,
629
+ fileSystem: fs,
630
+ globScanner: glob,
631
+ settings
632
+ };
633
+ return runCommand(command, deps);
604
634
  }
605
635
 
606
636
  // lib/cli/entry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A lightweight planning system for human-AI collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- Hello Claude, welcome to dust!
1
+ Hello Agent, welcome to dust!
2
2
 
3
3
  Your goal today is to make ONE SMALL CHANGE and then commit and push your changes.
4
4