@joshski/dust 0.1.1 → 0.1.2
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/dust.js +142 -87
- package/package.json +1 -1
package/dist/dust.js
CHANGED
|
@@ -2,11 +2,81 @@
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
|
|
5
|
+
// lib/cli/templates.ts
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
var templatesDir = join(__dirname2, "../templates");
|
|
11
|
+
function loadTemplate(name, variables = {}) {
|
|
12
|
+
const templatePath = join(templatesDir, `${name}.txt`);
|
|
13
|
+
let content = readFileSync(templatePath, "utf-8");
|
|
14
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
15
|
+
content = content.replaceAll(`{{${key}}}`, value);
|
|
16
|
+
}
|
|
17
|
+
return content;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// lib/cli/agent.ts
|
|
21
|
+
var AGENT_SUBCOMMANDS = [
|
|
22
|
+
"work",
|
|
23
|
+
"tasks",
|
|
24
|
+
"goals",
|
|
25
|
+
"ideas",
|
|
26
|
+
"help"
|
|
27
|
+
];
|
|
28
|
+
function generateAgentGreeting(settings) {
|
|
29
|
+
return loadTemplate("agent-greeting", { bin: settings.binaryPath });
|
|
30
|
+
}
|
|
31
|
+
function generateWorkInstructions(settings) {
|
|
32
|
+
return loadTemplate("agent-work", { bin: settings.binaryPath });
|
|
33
|
+
}
|
|
34
|
+
function generateTasksInstructions(settings) {
|
|
35
|
+
return loadTemplate("agent-tasks", { bin: settings.binaryPath });
|
|
36
|
+
}
|
|
37
|
+
function generateGoalsInstructions(settings) {
|
|
38
|
+
return loadTemplate("agent-goals", { bin: settings.binaryPath });
|
|
39
|
+
}
|
|
40
|
+
function generateIdeasInstructions(settings) {
|
|
41
|
+
return loadTemplate("agent-ideas", { bin: settings.binaryPath });
|
|
42
|
+
}
|
|
43
|
+
function generateAgentHelp(settings) {
|
|
44
|
+
return loadTemplate("agent-help", { bin: settings.binaryPath });
|
|
45
|
+
}
|
|
46
|
+
async function agent(ctx, args, settings) {
|
|
47
|
+
const subcommand = args[0];
|
|
48
|
+
if (!subcommand) {
|
|
49
|
+
ctx.stdout(generateAgentGreeting(settings));
|
|
50
|
+
return { exitCode: 0 };
|
|
51
|
+
}
|
|
52
|
+
switch (subcommand) {
|
|
53
|
+
case "work":
|
|
54
|
+
ctx.stdout(generateWorkInstructions(settings));
|
|
55
|
+
return { exitCode: 0 };
|
|
56
|
+
case "tasks":
|
|
57
|
+
ctx.stdout(generateTasksInstructions(settings));
|
|
58
|
+
return { exitCode: 0 };
|
|
59
|
+
case "goals":
|
|
60
|
+
ctx.stdout(generateGoalsInstructions(settings));
|
|
61
|
+
return { exitCode: 0 };
|
|
62
|
+
case "ideas":
|
|
63
|
+
ctx.stdout(generateIdeasInstructions(settings));
|
|
64
|
+
return { exitCode: 0 };
|
|
65
|
+
case "help":
|
|
66
|
+
ctx.stdout(generateAgentHelp(settings));
|
|
67
|
+
return { exitCode: 0 };
|
|
68
|
+
default:
|
|
69
|
+
ctx.stderr(`Unknown subcommand: ${subcommand}`);
|
|
70
|
+
ctx.stderr(`Available: ${AGENT_SUBCOMMANDS.join(", ")}`);
|
|
71
|
+
return { exitCode: 1 };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
5
75
|
// lib/cli/check.ts
|
|
6
76
|
import { spawn } from "node:child_process";
|
|
7
77
|
|
|
8
78
|
// lib/cli/validate.ts
|
|
9
|
-
import { dirname, resolve } from "node:path";
|
|
79
|
+
import { dirname as dirname2, resolve } from "node:path";
|
|
10
80
|
var REQUIRED_HEADINGS = ["## Goals", "## Blocked by", "## Definition of done"];
|
|
11
81
|
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
12
82
|
function validateFilename(filePath) {
|
|
@@ -36,7 +106,7 @@ function validateLinks(filePath, content, fs) {
|
|
|
36
106
|
const violations = [];
|
|
37
107
|
const lines = content.split(`
|
|
38
108
|
`);
|
|
39
|
-
const fileDir =
|
|
109
|
+
const fileDir = dirname2(filePath);
|
|
40
110
|
for (let i = 0;i < lines.length; i++) {
|
|
41
111
|
const line = lines[i];
|
|
42
112
|
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
@@ -59,6 +129,69 @@ function validateLinks(filePath, content, fs) {
|
|
|
59
129
|
}
|
|
60
130
|
return violations;
|
|
61
131
|
}
|
|
132
|
+
var SEMANTIC_RULES = [
|
|
133
|
+
{
|
|
134
|
+
section: "## Goals",
|
|
135
|
+
requiredPath: "/.dust/goals/",
|
|
136
|
+
description: "goal"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
section: "## Blocked by",
|
|
140
|
+
requiredPath: "/.dust/tasks/",
|
|
141
|
+
description: "task"
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
function validateSemanticLinks(filePath, content) {
|
|
145
|
+
const violations = [];
|
|
146
|
+
const lines = content.split(`
|
|
147
|
+
`);
|
|
148
|
+
const fileDir = dirname2(filePath);
|
|
149
|
+
let currentSection = null;
|
|
150
|
+
for (let i = 0;i < lines.length; i++) {
|
|
151
|
+
const line = lines[i];
|
|
152
|
+
if (line.startsWith("## ")) {
|
|
153
|
+
currentSection = line;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const rule = SEMANTIC_RULES.find((r) => r.section === currentSection);
|
|
157
|
+
if (!rule)
|
|
158
|
+
continue;
|
|
159
|
+
const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
160
|
+
let match = linkPattern.exec(line);
|
|
161
|
+
while (match) {
|
|
162
|
+
const linkTarget = match[2];
|
|
163
|
+
if (linkTarget.startsWith("#")) {
|
|
164
|
+
violations.push({
|
|
165
|
+
file: filePath,
|
|
166
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an anchor: "${linkTarget}"`,
|
|
167
|
+
line: i + 1
|
|
168
|
+
});
|
|
169
|
+
match = linkPattern.exec(line);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
173
|
+
violations.push({
|
|
174
|
+
file: filePath,
|
|
175
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an external URL: "${linkTarget}"`,
|
|
176
|
+
line: i + 1
|
|
177
|
+
});
|
|
178
|
+
match = linkPattern.exec(line);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const targetPath = linkTarget.split("#")[0];
|
|
182
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
183
|
+
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
184
|
+
violations.push({
|
|
185
|
+
file: filePath,
|
|
186
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file: "${linkTarget}"`,
|
|
187
|
+
line: i + 1
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
match = linkPattern.exec(line);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return violations;
|
|
194
|
+
}
|
|
62
195
|
async function validate(ctx, fs, _args, glob) {
|
|
63
196
|
const dustPath = `${ctx.cwd}/.dust`;
|
|
64
197
|
if (!fs.exists(dustPath)) {
|
|
@@ -88,6 +221,7 @@ async function validate(ctx, fs, _args, glob) {
|
|
|
88
221
|
violations.push(filenameViolation);
|
|
89
222
|
}
|
|
90
223
|
violations.push(...validateTaskHeadings(filePath, content));
|
|
224
|
+
violations.push(...validateSemanticLinks(filePath, content));
|
|
91
225
|
}
|
|
92
226
|
}
|
|
93
227
|
if (violations.length === 0) {
|
|
@@ -308,12 +442,12 @@ async function prompt(ctx, fs, args) {
|
|
|
308
442
|
}
|
|
309
443
|
|
|
310
444
|
// lib/cli/settings.ts
|
|
311
|
-
import { join } from "node:path";
|
|
445
|
+
import { join as join2 } from "node:path";
|
|
312
446
|
var DEFAULT_SETTINGS = {
|
|
313
447
|
binaryPath: "dust"
|
|
314
448
|
};
|
|
315
449
|
async function loadSettings(cwd, fs) {
|
|
316
|
-
const settingsPath =
|
|
450
|
+
const settingsPath = join2(cwd, ".dust", "config", "settings.json");
|
|
317
451
|
if (!fs.exists(settingsPath)) {
|
|
318
452
|
return DEFAULT_SETTINGS;
|
|
319
453
|
}
|
|
@@ -337,92 +471,11 @@ var COMMANDS = [
|
|
|
337
471
|
"list",
|
|
338
472
|
"next",
|
|
339
473
|
"check",
|
|
474
|
+
"agent",
|
|
340
475
|
"help"
|
|
341
476
|
];
|
|
342
477
|
function generateHelpText(settings) {
|
|
343
|
-
|
|
344
|
-
return `dust - A lightweight planning system for human-AI collaboration
|
|
345
|
-
|
|
346
|
-
Usage: ${bin} <command> [options]
|
|
347
|
-
|
|
348
|
-
Commands:
|
|
349
|
-
init Initialize a new Dust repository
|
|
350
|
-
prompt <name> Output a prompt by name (e.g., ${bin} prompt work)
|
|
351
|
-
validate Run validation checks on .dust/ files
|
|
352
|
-
list [type] List items (tasks, ideas, goals, facts)
|
|
353
|
-
next Show tasks ready to work on (not blocked)
|
|
354
|
-
check Run project-defined quality gate hook
|
|
355
|
-
help Show this help message
|
|
356
|
-
|
|
357
|
-
Examples:
|
|
358
|
-
${bin} init
|
|
359
|
-
${bin} prompt work
|
|
360
|
-
${bin} validate
|
|
361
|
-
${bin} list tasks
|
|
362
|
-
${bin} list
|
|
363
|
-
${bin} next
|
|
364
|
-
${bin} check
|
|
365
|
-
|
|
366
|
-
---
|
|
367
|
-
|
|
368
|
-
## Agent Guide
|
|
369
|
-
|
|
370
|
-
This section provides comprehensive guidance for AI agents working with dust.
|
|
371
|
-
|
|
372
|
-
### Directory Structure
|
|
373
|
-
|
|
374
|
-
The \`.dust/\` directory contains all planning artifacts:
|
|
375
|
-
|
|
376
|
-
- **\`.dust/goals/\`** - Mission statements and guiding principles
|
|
377
|
-
- **\`.dust/ideas/\`** - Future feature notes and proposals (intentionally vague)
|
|
378
|
-
- **\`.dust/tasks/\`** - Detailed work plans with dependencies and definitions of done
|
|
379
|
-
- **\`.dust/facts/\`** - Documentation of current system state and architecture
|
|
380
|
-
- **\`.dust/hooks/\`** - Executable scripts for quality gates (e.g., \`check\` hook)
|
|
381
|
-
|
|
382
|
-
All files are markdown with slug-style names (lowercase, hyphens, no spaces).
|
|
383
|
-
|
|
384
|
-
### Working on Tasks
|
|
385
|
-
|
|
386
|
-
**Run \`${bin} check\` before starting work** to verify the project is in a good state before making changes.
|
|
387
|
-
|
|
388
|
-
Run \`${bin} next\` to find tasks ready to work on. Each task file contains:
|
|
389
|
-
|
|
390
|
-
- \`## Goals\` - Links to goals this task supports
|
|
391
|
-
- \`## Blocked by\` - Tasks that must complete first (empty or "(none)" means ready)
|
|
392
|
-
- \`## Definition of done\` - Criteria for completion
|
|
393
|
-
|
|
394
|
-
A task is **unblocked** when its "Blocked by" section is empty, says "(none)", or all referenced task files have been deleted.
|
|
395
|
-
|
|
396
|
-
### Completing a Task
|
|
397
|
-
|
|
398
|
-
**Run \`${bin} check\` before committing** to ensure all quality gates pass.
|
|
399
|
-
|
|
400
|
-
When finishing a task, create a single atomic commit that includes:
|
|
401
|
-
|
|
402
|
-
1. All implementation changes
|
|
403
|
-
2. Deletion of the completed task file
|
|
404
|
-
3. Updates to any facts that changed
|
|
405
|
-
4. Deletion of any ideas that were fully realized
|
|
406
|
-
5. Updates to any tasks that referenced this one in their "Blocked by" sections
|
|
407
|
-
|
|
408
|
-
### Common Workflows
|
|
409
|
-
|
|
410
|
-
- **"Work on the next task"** - Run \`${bin} next\`, pick a task, implement it
|
|
411
|
-
- **"Work on task X"** - Implement \`.dust/tasks/X.md\` directly
|
|
412
|
-
- **"Convert idea Y to tasks"** - Break down \`.dust/ideas/Y.md\` into tasks
|
|
413
|
-
- **"Validate facts"** - Check \`.dust/facts/\` for accuracy against the codebase
|
|
414
|
-
|
|
415
|
-
### Configuring Agent Files
|
|
416
|
-
|
|
417
|
-
Projects using dust should add a minimal pointer to their agent configuration files (CLAUDE.md, AGENTS.md, etc.):
|
|
418
|
-
|
|
419
|
-
\`\`\`markdown
|
|
420
|
-
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
421
|
-
Always run \`dust help\` when you start working in this repository.
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
This approach keeps agent instructions minimal, ensures agents get current documentation, and reduces maintenance burden.
|
|
425
|
-
`;
|
|
478
|
+
return loadTemplate("help", { bin: settings.binaryPath });
|
|
426
479
|
}
|
|
427
480
|
var HELP_TEXT = generateHelpText({ binaryPath: "dust" });
|
|
428
481
|
function isHelpRequest(command) {
|
|
@@ -445,6 +498,8 @@ async function runCommand(command, commandArgs, ctx, fs, glob, settings) {
|
|
|
445
498
|
return next(ctx, fs, commandArgs);
|
|
446
499
|
case "check":
|
|
447
500
|
return check(ctx, fs, commandArgs, defaultProcessRunner, glob);
|
|
501
|
+
case "agent":
|
|
502
|
+
return agent(ctx, commandArgs, settings);
|
|
448
503
|
case "help":
|
|
449
504
|
ctx.stdout(generateHelpText(settings));
|
|
450
505
|
return { exitCode: 0 };
|