@joshski/dust 0.1.61 → 0.1.63

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.
@@ -1,4 +1,4 @@
1
- import type { ReadableFileSystem } from '../cli/types';
1
+ import type { ReadableFileSystem } from '../filesystem/types';
2
2
  export interface Fact {
3
3
  slug: string;
4
4
  title: string;
@@ -1,4 +1,4 @@
1
- import type { ReadableFileSystem } from '../cli/types';
1
+ import type { ReadableFileSystem } from '../filesystem/types';
2
2
  export interface IdeaOption {
3
3
  name: string;
4
4
  description: string;
@@ -1,11 +1,12 @@
1
- import type { FileSystem, ReadableFileSystem } from '../cli/types';
1
+ import type { FileSystem, ReadableFileSystem } from '../filesystem/types';
2
2
  import { type Fact } from './facts';
3
3
  import { type Idea, type IdeaOpenQuestion, type IdeaOption, parseOpenQuestions } from './ideas';
4
4
  import { type Principle } from './principles';
5
5
  import { type Task } from './tasks';
6
- import { type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
6
+ import { CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllCaptureIdeaTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
7
7
  export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, };
8
- export { parseOpenQuestions };
8
+ export { CAPTURE_IDEA_PREFIX, findAllCaptureIdeaTasks, parseOpenQuestions };
9
+ export type { IdeaInProgress };
9
10
  export interface ArtifactsRepository {
10
11
  parseIdea(options: {
11
12
  slug: string;
@@ -1,4 +1,4 @@
1
- import type { ReadableFileSystem } from '../cli/types';
1
+ import type { ReadableFileSystem } from '../filesystem/types';
2
2
  export interface Principle {
3
3
  slug: string;
4
4
  title: string;
@@ -1,4 +1,4 @@
1
- import type { ReadableFileSystem } from '../cli/types';
1
+ import type { ReadableFileSystem } from '../filesystem/types';
2
2
  export interface Task {
3
3
  slug: string;
4
4
  title: string;
@@ -1,4 +1,4 @@
1
- import type { FileSystem, ReadableFileSystem } from '../cli/types';
1
+ import type { FileSystem, ReadableFileSystem } from '../filesystem/types';
2
2
  export declare const IDEA_TRANSITION_PREFIXES: string[];
3
3
  export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
4
4
  export declare const BUILD_IDEA_PREFIX = "Build Idea: ";
package/dist/artifacts.js CHANGED
@@ -273,6 +273,32 @@ async function parseTask(fileSystem, dustPath, slug) {
273
273
  // lib/artifacts/workflow-tasks.ts
274
274
  var CAPTURE_IDEA_PREFIX = "Add Idea: ";
275
275
  var BUILD_IDEA_PREFIX = "Build Idea: ";
276
+ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
277
+ const tasksPath = `${dustPath}/tasks`;
278
+ if (!fileSystem.exists(tasksPath))
279
+ return [];
280
+ const files = await fileSystem.readdir(tasksPath);
281
+ const results = [];
282
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
283
+ const content = await fileSystem.readFile(`${tasksPath}/${file}`);
284
+ const titleMatch = content.match(/^#\s+(.+)$/m);
285
+ if (!titleMatch)
286
+ continue;
287
+ const title = titleMatch[1].trim();
288
+ if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
289
+ results.push({
290
+ taskSlug: file.replace(/\.md$/, ""),
291
+ ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
292
+ });
293
+ } else if (title.startsWith(BUILD_IDEA_PREFIX)) {
294
+ results.push({
295
+ taskSlug: file.replace(/\.md$/, ""),
296
+ ideaTitle: title.slice(BUILD_IDEA_PREFIX.length)
297
+ });
298
+ }
299
+ }
300
+ return results;
301
+ }
276
302
  function titleToFilename(title) {
277
303
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
278
304
  }
@@ -594,6 +620,8 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
594
620
  }
595
621
  export {
596
622
  parseOpenQuestions,
623
+ findAllCaptureIdeaTasks,
597
624
  buildReadOnlyArtifactsRepository,
598
- buildArtifactsRepository
625
+ buildArtifactsRepository,
626
+ CAPTURE_IDEA_PREFIX
599
627
  };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Audits repository - programmatic access to audit templates.
3
+ *
4
+ * Audits are canned tasks that help maintain project health.
5
+ * Sources:
6
+ * 1. User-configured audits in .dust/config/audits/*.md (takes precedence)
7
+ * 2. Stock audits from lib/audits/stock-audits.ts
8
+ */
9
+ import type { FileSystem } from '../filesystem/types';
10
+ export { loadStockAudits } from './stock-audits';
11
+ export interface Audit {
12
+ name: string;
13
+ title: string;
14
+ description: string;
15
+ template: string;
16
+ source: 'stock' | string;
17
+ }
18
+ export interface CreateAuditTaskResult {
19
+ filePath: string;
20
+ relativePath: string;
21
+ }
22
+ export interface AuditsRepository {
23
+ listAudits(): Promise<Audit[]>;
24
+ parseAudit(options: {
25
+ name: string;
26
+ }): Promise<Audit>;
27
+ createAuditTask(options: {
28
+ name: string;
29
+ }): Promise<CreateAuditTaskResult>;
30
+ }
31
+ /**
32
+ * Transforms audit template content for the task file.
33
+ * Changes the title from "# Original Title" to "# Audit: Original Title"
34
+ */
35
+ export declare function transformAuditContent(content: string): string;
36
+ export declare function buildAuditsRepository(fileSystem: FileSystem, dustPath: string): AuditsRepository;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Stock audit templates as type-safe functions.
3
+ *
4
+ * Users can override any of these by placing a file with the same name
5
+ * in .dust/config/audits/.
6
+ */
7
+ export interface StockAudit {
8
+ name: string;
9
+ description: string;
10
+ template: string;
11
+ }
12
+ export declare function loadStockAudits(): StockAudit[];
package/dist/audits.js ADDED
@@ -0,0 +1,459 @@
1
+ // lib/audits/index.ts
2
+ import { basename } from "node:path";
3
+
4
+ // lib/markdown/markdown-utilities.ts
5
+ function extractTitle(content) {
6
+ const match = content.match(/^#\s+(.+)$/m);
7
+ return match ? match[1].trim() : null;
8
+ }
9
+ function extractOpeningSentence(content) {
10
+ const lines = content.split(`
11
+ `);
12
+ let h1Index = -1;
13
+ for (let i = 0;i < lines.length; i++) {
14
+ if (lines[i].match(/^#\s+.+$/)) {
15
+ h1Index = i;
16
+ break;
17
+ }
18
+ }
19
+ if (h1Index === -1) {
20
+ return null;
21
+ }
22
+ let paragraphStart = -1;
23
+ for (let i = h1Index + 1;i < lines.length; i++) {
24
+ const line = lines[i].trim();
25
+ if (line !== "") {
26
+ paragraphStart = i;
27
+ break;
28
+ }
29
+ }
30
+ if (paragraphStart === -1) {
31
+ return null;
32
+ }
33
+ const firstLine = lines[paragraphStart];
34
+ const trimmedFirstLine = firstLine.trim();
35
+ if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
36
+ return null;
37
+ }
38
+ let paragraph = "";
39
+ for (let i = paragraphStart;i < lines.length; i++) {
40
+ const line = lines[i].trim();
41
+ if (line === "")
42
+ break;
43
+ if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
44
+ break;
45
+ }
46
+ paragraph += (paragraph ? " " : "") + line;
47
+ }
48
+ const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
49
+ if (!sentenceMatch) {
50
+ return null;
51
+ }
52
+ return sentenceMatch[1];
53
+ }
54
+
55
+ // lib/cli/dedent.ts
56
+ function dedent(strings, ...values) {
57
+ const result = strings.reduce((acc, part, index) => acc + part + (values[index] ?? ""), "");
58
+ const lines = result.split(`
59
+ `);
60
+ const indent = lines.filter((line) => line.trim()).reduce((min, line) => Math.min(min, line.match(/^\s*/)[0].length), Number.POSITIVE_INFINITY);
61
+ return lines.map((line) => line.slice(indent)).join(`
62
+ `).trim();
63
+ }
64
+
65
+ // lib/audits/stock-audits.ts
66
+ function agentDeveloperExperience() {
67
+ return dedent`
68
+ # Agent Developer Experience
69
+
70
+ Review the codebase to ensure agents have everything they need to operate effectively. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
71
+
72
+ ## Scope
73
+
74
+ Focus on these areas:
75
+
76
+ 1. **Context window efficiency** - Are files small and well-organized?
77
+ 2. **Test coverage** - Can agents verify correctness through tests?
78
+ 3. **Feedback loop speed** - How fast are checks and tests?
79
+ 4. **Debugging tools** - Can agents diagnose issues without trial and error?
80
+ 5. **Structured logging** - Is system behavior observable through logs?
81
+
82
+ ## Principles
83
+
84
+ (none)
85
+
86
+ ## Blocked By
87
+
88
+ (none)
89
+
90
+ ## Definition of Done
91
+
92
+ - [ ] Reviewed file sizes and organization for context window fit
93
+ - [ ] Verified test coverage is sufficient for agent verification
94
+ - [ ] Measured feedback loop speed (time from change to check result)
95
+ - [ ] Confirmed debugging tools and structured logging are in place
96
+ - [ ] Proposed ideas for any improvements identified
97
+ `;
98
+ }
99
+ function deadCode() {
100
+ return dedent`
101
+ # Dead Code
102
+
103
+ Find and remove unused code to improve maintainability and reduce bundle size. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
104
+
105
+ ## Scope
106
+
107
+ Focus on these areas:
108
+
109
+ 1. **Unused exports** - Functions, classes, constants that are never imported
110
+ 2. **Unreachable code** - Code after return statements, impossible conditions
111
+ 3. **Orphaned files** - Files that are not imported anywhere
112
+ 4. **Unused dependencies** - Packages in package.json not used in code
113
+ 5. **Commented-out code** - Old code left in comments
114
+
115
+ ## Principles
116
+
117
+ (none)
118
+
119
+ ## Blocked By
120
+
121
+ (none)
122
+
123
+ ## Definition of Done
124
+
125
+ - [ ] Ran static analysis tools to find unused exports
126
+ - [ ] Identified files with no incoming imports
127
+ - [ ] Listed unused dependencies
128
+ - [ ] Reviewed commented-out code blocks
129
+ - [ ] Created list of code safe to remove
130
+ - [ ] Verified removal won't break dynamic imports or reflection
131
+ - [ ] Proposed ideas for any dead code worth removing
132
+ `;
133
+ }
134
+ function factsVerification() {
135
+ return dedent`
136
+ # Facts Verification
137
+
138
+ Review \`.dust/facts/\` to ensure documented facts match current reality. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
139
+
140
+ ## Scope
141
+
142
+ Focus on these areas:
143
+
144
+ 1. **Accuracy** - Do documented facts reflect the current codebase?
145
+ 2. **Completeness** - Are important implementation details documented?
146
+ 3. **Staleness** - Have facts become outdated due to recent changes?
147
+ 4. **Relevance** - Are all facts still useful for understanding the project?
148
+
149
+ ## Principles
150
+
151
+ (none)
152
+
153
+ ## Blocked By
154
+
155
+ (none)
156
+
157
+ ## Definition of Done
158
+
159
+ - [ ] Read each fact file in \`.dust/facts/\`
160
+ - [ ] Verified each fact against current codebase
161
+ - [ ] Identified outdated or inaccurate facts
162
+ - [ ] Listed missing facts that would help agents
163
+ - [ ] Updated or removed stale facts
164
+ - [ ] Proposed ideas for any facts improvements needed
165
+ `;
166
+ }
167
+ function ideasFromCommits() {
168
+ return dedent`
169
+ # Ideas from Commits
170
+
171
+ Review recent commit history to identify follow-up improvement ideas. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues or opportunities you identify, avoiding duplication.
172
+
173
+ ## Scope
174
+
175
+ Focus on these areas:
176
+
177
+ 1. **Technical debt** - Did recent work introduce shortcuts?
178
+ 2. **Incomplete work** - Are there TODO comments or partial implementations?
179
+ 3. **Pattern opportunities** - Can recent changes be generalized?
180
+ 4. **Test gaps** - Do recent changes have adequate test coverage?
181
+
182
+ ## Principles
183
+
184
+ (none)
185
+
186
+ ## Blocked By
187
+
188
+ (none)
189
+
190
+ ## Definition of Done
191
+
192
+ - [ ] Reviewed commits from the last 20 commits
193
+ - [ ] Identified patterns or shortcuts worth addressing
194
+ - [ ] Listed TODO comments added in recent commits
195
+ - [ ] Noted areas where changes could be generalized
196
+ - [ ] Proposed follow-up ideas for any issues identified
197
+ `;
198
+ }
199
+ function ideasFromPrinciples() {
200
+ return dedent`
201
+ # Ideas from Principles
202
+
203
+ Review \`.dust/principles/\` to generate new improvement ideas. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues or opportunities you identify, avoiding duplication.
204
+
205
+ ## Scope
206
+
207
+ Focus on these areas:
208
+
209
+ 1. **Unmet principles** - Which principles lack supporting work?
210
+ 2. **Gap analysis** - Where does the codebase fall short of principles?
211
+ 3. **New opportunities** - What work would better achieve each principle?
212
+ 4. **Principle alignment** - Are current tasks aligned with stated principles?
213
+
214
+ ## Principles
215
+
216
+ (none)
217
+
218
+ ## Blocked By
219
+
220
+ (none)
221
+
222
+ ## Definition of Done
223
+
224
+ - [ ] Read each principle file in \`.dust/principles/\`
225
+ - [ ] Analyzed codebase for alignment with each principle
226
+ - [ ] Listed gaps between current state and principle intent
227
+ - [ ] Proposed new ideas for unmet or underserved principles
228
+ `;
229
+ }
230
+ function performanceReview() {
231
+ return dedent`
232
+ # Performance Review
233
+
234
+ Review the application for performance issues and optimization opportunities. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
235
+
236
+ ## Scope
237
+
238
+ Focus on these areas:
239
+
240
+ 1. **Startup time** - How fast does the application start?
241
+ 2. **Command latency** - How responsive are CLI commands?
242
+ 3. **Memory usage** - Is memory being used efficiently?
243
+ 4. **Build performance** - How fast is the build process?
244
+ 5. **Test speed** - Are tests running efficiently?
245
+
246
+ ## Principles
247
+
248
+ (none)
249
+
250
+ ## Blocked By
251
+
252
+ (none)
253
+
254
+ ## Definition of Done
255
+
256
+ - [ ] Measured startup time for common commands
257
+ - [ ] Profiled memory usage during typical operations
258
+ - [ ] Identified slow commands or operations
259
+ - [ ] Listed optimization opportunities by impact
260
+ - [ ] Proposed ideas for any performance improvements identified
261
+ `;
262
+ }
263
+ function securityReview() {
264
+ return dedent`
265
+ # Security Review
266
+
267
+ Review the codebase for common security vulnerabilities and misconfigurations. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
268
+
269
+ ## Scope
270
+
271
+ Focus on these areas:
272
+
273
+ 1. **Hardcoded secrets** - API keys, passwords, tokens in source code
274
+ 2. **Injection vulnerabilities** - SQL injection, command injection, XSS
275
+ 3. **Authentication issues** - Weak password handling, missing auth checks
276
+ 4. **Sensitive data exposure** - Logging sensitive data, insecure storage
277
+ 5. **Dependency vulnerabilities** - Known CVEs in dependencies
278
+
279
+ ## Principles
280
+
281
+ (none)
282
+
283
+ ## Blocked By
284
+
285
+ (none)
286
+
287
+ ## Definition of Done
288
+
289
+ - [ ] Searched for hardcoded secrets (API keys, passwords, tokens)
290
+ - [ ] Reviewed input validation and sanitization
291
+ - [ ] Checked authentication and authorization logic
292
+ - [ ] Verified sensitive data is not logged or exposed
293
+ - [ ] Ran dependency audit for known vulnerabilities
294
+ - [ ] Documented any findings with severity ratings
295
+ - [ ] Proposed ideas for any security issues found
296
+ `;
297
+ }
298
+ function staleIdeas() {
299
+ return dedent`
300
+ # Stale Ideas
301
+
302
+ Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
303
+
304
+ ## Scope
305
+
306
+ Focus on these areas:
307
+
308
+ 1. **Age** - Ideas unchanged for many commits may need attention
309
+ 2. **Relevance** - Has the project evolved past the idea?
310
+ 3. **Actionability** - Can the idea be converted to a task?
311
+ 4. **Duplication** - Are there overlapping or redundant ideas?
312
+
313
+ ## Principles
314
+
315
+ (none)
316
+
317
+ ## Blocked By
318
+
319
+ (none)
320
+
321
+ ## Definition of Done
322
+
323
+ - [ ] Listed all ideas with their last modification date
324
+ - [ ] Identified ideas unchanged for 50+ commits
325
+ - [ ] Reviewed each stale idea for current relevance
326
+ - [ ] Promoted actionable ideas to tasks
327
+ - [ ] Deleted ideas that are no longer relevant
328
+ `;
329
+ }
330
+ function testCoverage() {
331
+ return dedent`
332
+ # Test Coverage
333
+
334
+ Identify untested code paths and areas that need additional test coverage. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues you identify, avoiding duplication.
335
+
336
+ ## Scope
337
+
338
+ Focus on these areas:
339
+
340
+ 1. **Core business logic** - Functions that handle critical operations
341
+ 2. **Edge cases** - Boundary conditions, error handling paths
342
+ 3. **Integration points** - API endpoints, database operations
343
+ 4. **User-facing features** - UI components, form validation
344
+ 5. **Recent changes** - Code modified in the last few commits
345
+
346
+ ## Principles
347
+
348
+ (none)
349
+
350
+ ## Blocked By
351
+
352
+ (none)
353
+
354
+ ## Definition of Done
355
+
356
+ - [ ] Identified modules with low or no test coverage
357
+ - [ ] Listed critical paths that lack tests
358
+ - [ ] Prioritized areas by risk and importance
359
+ - [ ] Proposed ideas for any test coverage gaps identified
360
+ `;
361
+ }
362
+ var stockAuditFunctions = {
363
+ "agent-developer-experience": agentDeveloperExperience,
364
+ "dead-code": deadCode,
365
+ "facts-verification": factsVerification,
366
+ "ideas-from-commits": ideasFromCommits,
367
+ "ideas-from-principles": ideasFromPrinciples,
368
+ "performance-review": performanceReview,
369
+ "security-review": securityReview,
370
+ "stale-ideas": staleIdeas,
371
+ "test-coverage": testCoverage
372
+ };
373
+ function loadStockAudits() {
374
+ return Object.entries(stockAuditFunctions).sort(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
375
+ const template = render();
376
+ const description = extractOpeningSentence(template);
377
+ return { name, description, template };
378
+ });
379
+ }
380
+
381
+ // lib/audits/index.ts
382
+ function transformAuditContent(content) {
383
+ const titleMatch = content.match(/^#\s+(.+)$/m);
384
+ if (!titleMatch) {
385
+ return content;
386
+ }
387
+ const originalTitle = titleMatch[1];
388
+ return content.replace(/^#\s+.+$/m, `# Audit: ${originalTitle}`);
389
+ }
390
+ function buildAuditsRepository(fileSystem, dustPath) {
391
+ const userAuditsPath = `${dustPath}/config/audits`;
392
+ const tasksPath = `${dustPath}/tasks`;
393
+ async function loadAllAudits() {
394
+ const audits = new Map;
395
+ for (const stockAudit of loadStockAudits()) {
396
+ audits.set(stockAudit.name, {
397
+ name: stockAudit.name,
398
+ title: extractTitle(stockAudit.template),
399
+ description: stockAudit.description,
400
+ template: stockAudit.template,
401
+ source: "stock"
402
+ });
403
+ }
404
+ if (fileSystem.exists(userAuditsPath)) {
405
+ const files = await fileSystem.readdir(userAuditsPath);
406
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
407
+ for (const file of mdFiles) {
408
+ const name = basename(file, ".md");
409
+ const filePath = `${userAuditsPath}/${file}`;
410
+ const content = await fileSystem.readFile(filePath);
411
+ const title = extractTitle(content) || name;
412
+ const description = extractOpeningSentence(content) || "";
413
+ const relativePath = `.dust/config/audits/${file}`;
414
+ audits.set(name, {
415
+ name,
416
+ title,
417
+ description,
418
+ template: content,
419
+ source: relativePath
420
+ });
421
+ }
422
+ }
423
+ return audits;
424
+ }
425
+ return {
426
+ async listAudits() {
427
+ const auditsMap = await loadAllAudits();
428
+ return Array.from(auditsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
429
+ },
430
+ async parseAudit(options) {
431
+ const auditsMap = await loadAllAudits();
432
+ const audit = auditsMap.get(options.name);
433
+ if (!audit) {
434
+ throw new Error(`Audit not found: "${options.name}"`);
435
+ }
436
+ return audit;
437
+ },
438
+ async createAuditTask(options) {
439
+ const audit = await this.parseAudit(options);
440
+ const taskFilePath = `${tasksPath}/audit-${options.name}.md`;
441
+ const relativeTaskPath = `.dust/tasks/audit-${options.name}.md`;
442
+ if (fileSystem.exists(taskFilePath)) {
443
+ throw new Error(`Audit task already exists at ${relativeTaskPath}`);
444
+ }
445
+ const transformedContent = transformAuditContent(audit.template);
446
+ await fileSystem.mkdir(tasksPath, { recursive: true });
447
+ await fileSystem.writeFile(taskFilePath, transformedContent);
448
+ return {
449
+ filePath: taskFilePath,
450
+ relativePath: relativeTaskPath
451
+ };
452
+ }
453
+ };
454
+ }
455
+ export {
456
+ transformAuditContent,
457
+ loadStockAudits,
458
+ buildAuditsRepository
459
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Dedent tagged template literal helper
3
+ *
4
+ * Strips common leading whitespace from multi-line template literals,
5
+ * making it possible to write properly indented code while producing
6
+ * clean output.
7
+ */
8
+ export declare function dedent(strings: TemplateStringsArray, ...values: unknown[]): string;
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Common types for CLI commands
3
3
  */
4
+ import type { FileSystem, GlobScanner } from '../filesystem/types';
5
+ export type { FileSystem, GlobScanner, ReadableFileSystem, WriteOptions, } from '../filesystem/types';
4
6
  export interface CommandContext {
5
7
  cwd: string;
6
8
  stdout: (message: string) => void;
@@ -10,27 +12,6 @@ export interface CommandContext {
10
12
  export interface CommandResult {
11
13
  exitCode: number;
12
14
  }
13
- export interface WriteOptions {
14
- flag?: 'w' | 'wx';
15
- }
16
- export interface ReadableFileSystem {
17
- exists: (path: string) => boolean;
18
- readFile: (path: string) => Promise<string>;
19
- readdir: (path: string) => Promise<string[]>;
20
- isDirectory: (path: string) => boolean;
21
- }
22
- export interface FileSystem extends ReadableFileSystem {
23
- writeFile: (path: string, content: string, options?: WriteOptions) => Promise<void>;
24
- mkdir: (path: string, options?: {
25
- recursive?: boolean;
26
- }) => Promise<void>;
27
- chmod: (path: string, mode: number) => Promise<void>;
28
- getFileCreationTime: (path: string) => number;
29
- rename: (oldPath: string, newPath: string) => Promise<void>;
30
- }
31
- export interface GlobScanner {
32
- scan: (dir: string) => AsyncIterable<string>;
33
- }
34
15
  export interface CheckConfig {
35
16
  name: string;
36
17
  command: string;
package/dist/dust.js CHANGED
@@ -274,6 +274,9 @@ async function loadSettings(cwd, fileSystem) {
274
274
  }
275
275
  }
276
276
 
277
+ // lib/version.ts
278
+ var DUST_VERSION = "0.1.63";
279
+
277
280
  // lib/cli/dedent.ts
278
281
  function dedent(strings, ...values) {
279
282
  const result = strings.reduce((acc, part, index) => acc + part + (values[index] ?? ""), "");
@@ -902,6 +905,16 @@ function loadStockAudits() {
902
905
  });
903
906
  }
904
907
 
908
+ // lib/audits/index.ts
909
+ function transformAuditContent(content) {
910
+ const titleMatch = content.match(/^#\s+(.+)$/m);
911
+ if (!titleMatch) {
912
+ return content;
913
+ }
914
+ const originalTitle = titleMatch[1];
915
+ return content.replace(/^#\s+.+$/m, `# Audit: ${originalTitle}`);
916
+ }
917
+
905
918
  // lib/cli/colors.ts
906
919
  var ANSI_COLORS = {
907
920
  reset: "\x1B[0m",
@@ -936,14 +949,6 @@ function getColors() {
936
949
  }
937
950
 
938
951
  // lib/cli/commands/audit.ts
939
- function transformAuditContent(content) {
940
- const titleMatch = content.match(/^#\s+(.+)$/m);
941
- if (!titleMatch) {
942
- return content;
943
- }
944
- const originalTitle = titleMatch[1];
945
- return content.replace(/^#\s+.+$/m, `# Audit: ${originalTitle}`);
946
- }
947
952
  async function addAudit(auditName, dependencies) {
948
953
  const { context, fileSystem, settings } = dependencies;
949
954
  const dustPath = `${context.cwd}/.dust`;
@@ -1034,7 +1039,6 @@ import { accessSync, statSync } from "node:fs";
1034
1039
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1035
1040
  import { createServer as httpCreateServer } from "node:http";
1036
1041
  import { homedir } from "node:os";
1037
- import { join as join9 } from "node:path";
1038
1042
 
1039
1043
  // lib/bucket/auth.ts
1040
1044
  import { join as join4 } from "node:path";
@@ -1202,8 +1206,14 @@ function getLogLines(buffer) {
1202
1206
  return buffer.lines;
1203
1207
  }
1204
1208
 
1209
+ // lib/bucket/paths.ts
1210
+ import { join as join5 } from "node:path";
1211
+ function getReposDir(env, homeDir) {
1212
+ return env.DUST_REPOS_DIR || join5(homeDir, ".dust", "repos");
1213
+ }
1214
+
1205
1215
  // lib/bucket/repository.ts
1206
- import { dirname as dirname3, join as join8 } from "node:path";
1216
+ import { dirname as dirname2 } from "node:path";
1207
1217
 
1208
1218
  // lib/claude/spawn-claude-code.ts
1209
1219
  import { spawn as nodeSpawn } from "node:child_process";
@@ -1622,7 +1632,7 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1622
1632
  }
1623
1633
 
1624
1634
  // lib/logging/index.ts
1625
- import { join as join5 } from "node:path";
1635
+ import { join as join6 } from "node:path";
1626
1636
 
1627
1637
  // lib/logging/match.ts
1628
1638
  function parsePatterns(debug) {
@@ -1706,8 +1716,8 @@ function createLoggingService() {
1706
1716
  return {
1707
1717
  enableFileLogs(scope, sinkForTesting) {
1708
1718
  const existing = process.env[DUST_LOG_FILE];
1709
- const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
1710
- const path = existing ?? join5(logDir, `${scope}.log`);
1719
+ const logDir = process.env.DUST_LOG_DIR ?? join6(process.cwd(), "log");
1720
+ const path = existing ?? join6(logDir, `${scope}.log`);
1711
1721
  if (!existing) {
1712
1722
  process.env[DUST_LOG_FILE] = path;
1713
1723
  }
@@ -1747,10 +1757,10 @@ var createLogger = defaultService.createLogger.bind(defaultService);
1747
1757
  var isEnabled = defaultService.isEnabled.bind(defaultService);
1748
1758
 
1749
1759
  // lib/bucket/repository-git.ts
1750
- import { join as join6 } from "node:path";
1760
+ import { join as join7 } from "node:path";
1751
1761
  function getRepoPath(repoName, reposDir) {
1752
1762
  const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
1753
- return join6(reposDir, safeName);
1763
+ return join7(reposDir, safeName);
1754
1764
  }
1755
1765
  async function cloneRepository(repository, targetPath, spawn, context) {
1756
1766
  return new Promise((resolve) => {
@@ -1818,10 +1828,7 @@ function formatAgentEvent(event) {
1818
1828
 
1819
1829
  // lib/cli/commands/loop.ts
1820
1830
  import { spawn as nodeSpawn2 } from "node:child_process";
1821
- import { readFileSync } from "node:fs";
1822
1831
  import os from "node:os";
1823
- import { dirname as dirname2, join as join7 } from "node:path";
1824
- import { fileURLToPath } from "node:url";
1825
1832
 
1826
1833
  // lib/artifacts/workflow-tasks.ts
1827
1834
  var IDEA_TRANSITION_PREFIXES = [
@@ -1970,26 +1977,12 @@ async function next(dependencies) {
1970
1977
  }
1971
1978
 
1972
1979
  // lib/cli/commands/loop.ts
1973
- var __dirname2 = dirname2(fileURLToPath(import.meta.url));
1974
- function getDustVersion() {
1975
- const candidates = [
1976
- join7(__dirname2, "../../../package.json"),
1977
- join7(__dirname2, "../package.json")
1978
- ];
1979
- for (const candidate of candidates) {
1980
- try {
1981
- const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
1982
- return packageJson.version ?? "unknown";
1983
- } catch {}
1984
- }
1985
- return "unknown";
1986
- }
1987
1980
  function getEnvironmentContext(cwd) {
1988
1981
  return {
1989
1982
  machineName: os.hostname(),
1990
1983
  cwd,
1991
1984
  platform: `${os.platform()} ${os.release()}`,
1992
- dustVersion: getDustVersion(),
1985
+ dustVersion: DUST_VERSION,
1993
1986
  runtimeVersion: process.version
1994
1987
  };
1995
1988
  }
@@ -2098,7 +2091,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
2098
2091
  const { context } = dependencies;
2099
2092
  const { spawn, run: run2 } = loopDependencies;
2100
2093
  const agentName = loopDependencies.agentType === "codex" ? "Codex" : "Claude";
2101
- const { onRawEvent, hooksInstalled = false, signal } = options;
2094
+ const { onRawEvent, hooksInstalled = false, signal, logger = log } = options;
2102
2095
  log("syncing with remote");
2103
2096
  onLoopEvent({ type: "loop.syncing" });
2104
2097
  const pullResult = await gitPull(context.cwd, spawn);
@@ -2199,7 +2192,7 @@ ${instructions}`;
2199
2192
  return "ran_claude";
2200
2193
  } catch (error) {
2201
2194
  const errorMessage = error instanceof Error ? error.message : String(error);
2202
- log(`${agentName} error on task ${task.title ?? task.path}: ${errorMessage}`);
2195
+ logger(`${agentName} error on task ${task.title ?? task.path}: ${errorMessage}`);
2203
2196
  context.stderr(`${agentName} exited with error: ${errorMessage}`);
2204
2197
  onAgentEvent?.({
2205
2198
  type: "agent-session-ended",
@@ -2475,7 +2468,7 @@ async function addRepository(repository, manager, repoDeps, context) {
2475
2468
  }
2476
2469
  log3(`adding repository ${repository.name}`);
2477
2470
  const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
2478
- await repoDeps.fileSystem.mkdir(dirname3(repoPath), { recursive: true });
2471
+ await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
2479
2472
  if (repoDeps.fileSystem.exists(repoPath)) {
2480
2473
  await removeRepository(repoPath, repoDeps.spawn, context);
2481
2474
  }
@@ -3056,11 +3049,11 @@ function defaultOpenBrowser(url) {
3056
3049
  const cmd = process.platform === "darwin" ? "open" : "xdg-open";
3057
3050
  nodeSpawn3(cmd, [url], { stdio: "ignore", detached: true }).unref();
3058
3051
  }
3059
- function createDefaultBucketDependencies() {
3060
- const authFileSystem = {
3052
+ function createAuthFileSystem(dependencies) {
3053
+ return {
3061
3054
  exists: (path) => {
3062
3055
  try {
3063
- accessSync(path);
3056
+ dependencies.accessSync(path);
3064
3057
  return true;
3065
3058
  } catch {
3066
3059
  return false;
@@ -3068,19 +3061,31 @@ function createDefaultBucketDependencies() {
3068
3061
  },
3069
3062
  isDirectory: (path) => {
3070
3063
  try {
3071
- return statSync(path).isDirectory();
3064
+ return dependencies.statSync(path).isDirectory();
3072
3065
  } catch {
3073
3066
  return false;
3074
3067
  }
3075
3068
  },
3076
- getFileCreationTime: (path) => statSync(path).birthtimeMs,
3077
- readFile: (path) => readFile(path, "utf8"),
3078
- writeFile: (path, content) => writeFile(path, content, "utf8"),
3079
- mkdir: (path, options) => mkdir(path, options).then(() => {}),
3080
- readdir: (path) => readdir(path),
3081
- chmod: (path, mode) => chmod(path, mode),
3082
- rename: (oldPath, newPath) => import("node:fs/promises").then((mod) => mod.rename(oldPath, newPath))
3069
+ getFileCreationTime: (path) => dependencies.statSync(path).birthtimeMs,
3070
+ readFile: (path) => dependencies.readFile(path, "utf8"),
3071
+ writeFile: (path, content) => dependencies.writeFile(path, content, "utf8"),
3072
+ mkdir: (path, options) => dependencies.mkdir(path, options).then(() => {}),
3073
+ readdir: (path) => dependencies.readdir(path),
3074
+ chmod: (path, mode) => dependencies.chmod(path, mode),
3075
+ rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
3083
3076
  };
3077
+ }
3078
+ function createDefaultBucketDependencies() {
3079
+ const authFileSystem = createAuthFileSystem({
3080
+ accessSync,
3081
+ statSync,
3082
+ readFile,
3083
+ writeFile,
3084
+ mkdir,
3085
+ readdir,
3086
+ chmod,
3087
+ rename: (oldPath, newPath) => import("node:fs/promises").then((mod) => mod.rename(oldPath, newPath))
3088
+ });
3084
3089
  return {
3085
3090
  spawn: nodeSpawn3,
3086
3091
  createWebSocket: defaultCreateWebSocket,
@@ -3091,7 +3096,7 @@ function createDefaultBucketDependencies() {
3091
3096
  writeStdout: defaultWriteStdout,
3092
3097
  isTTY: process.stdout.isTTY ?? false,
3093
3098
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
3094
- getReposDir: () => process.env.DUST_REPOS_DIR || join9(homedir(), ".dust", "repos"),
3099
+ getReposDir: () => getReposDir(process.env, homedir()),
3095
3100
  auth: {
3096
3101
  createServer: defaultCreateServer,
3097
3102
  openBrowser: defaultOpenBrowser,
@@ -3517,7 +3522,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3517
3522
  }
3518
3523
 
3519
3524
  // lib/cli/commands/lint-markdown.ts
3520
- import { join as join10 } from "node:path";
3525
+ import { join as join8 } from "node:path";
3521
3526
 
3522
3527
  // lib/lint/validators/content-validator.ts
3523
3528
  var REQUIRED_HEADINGS = [
@@ -3807,7 +3812,7 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
3807
3812
  }
3808
3813
 
3809
3814
  // lib/lint/validators/link-validator.ts
3810
- import { dirname as dirname4, resolve } from "node:path";
3815
+ import { dirname as dirname3, resolve } from "node:path";
3811
3816
  var SEMANTIC_RULES = [
3812
3817
  {
3813
3818
  section: "## Principles",
@@ -3824,7 +3829,7 @@ function validateLinks(filePath, content, fileSystem) {
3824
3829
  const violations = [];
3825
3830
  const lines = content.split(`
3826
3831
  `);
3827
- const fileDir = dirname4(filePath);
3832
+ const fileDir = dirname3(filePath);
3828
3833
  for (let i = 0;i < lines.length; i++) {
3829
3834
  const line = lines[i];
3830
3835
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3851,7 +3856,7 @@ function validateSemanticLinks(filePath, content) {
3851
3856
  const violations = [];
3852
3857
  const lines = content.split(`
3853
3858
  `);
3854
- const fileDir = dirname4(filePath);
3859
+ const fileDir = dirname3(filePath);
3855
3860
  let currentSection = null;
3856
3861
  for (let i = 0;i < lines.length; i++) {
3857
3862
  const line = lines[i];
@@ -3902,7 +3907,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
3902
3907
  const violations = [];
3903
3908
  const lines = content.split(`
3904
3909
  `);
3905
- const fileDir = dirname4(filePath);
3910
+ const fileDir = dirname3(filePath);
3906
3911
  let currentSection = null;
3907
3912
  for (let i = 0;i < lines.length; i++) {
3908
3913
  const line = lines[i];
@@ -3951,7 +3956,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
3951
3956
  }
3952
3957
 
3953
3958
  // lib/lint/validators/principle-hierarchy.ts
3954
- import { dirname as dirname5, resolve as resolve2 } from "node:path";
3959
+ import { dirname as dirname4, resolve as resolve2 } from "node:path";
3955
3960
  var REQUIRED_PRINCIPLE_HEADINGS = ["## Parent Principle", "## Sub-Principles"];
3956
3961
  function validatePrincipleHierarchySections(filePath, content) {
3957
3962
  const violations = [];
@@ -3968,7 +3973,7 @@ function validatePrincipleHierarchySections(filePath, content) {
3968
3973
  function extractPrincipleRelationships(filePath, content) {
3969
3974
  const lines = content.split(`
3970
3975
  `);
3971
- const fileDir = dirname5(filePath);
3976
+ const fileDir = dirname4(filePath);
3972
3977
  const parentPrinciples = [];
3973
3978
  const subPrinciples = [];
3974
3979
  let currentSection = null;
@@ -4089,7 +4094,7 @@ async function lintMarkdown(dependencies) {
4089
4094
  const violations = [];
4090
4095
  context.stdout("Validating directory structure...");
4091
4096
  violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
4092
- const settingsPath = join10(dustPath, "config", "settings.json");
4097
+ const settingsPath = join8(dustPath, "config", "settings.json");
4093
4098
  if (fileSystem.exists(settingsPath)) {
4094
4099
  context.stdout("Validating settings.json...");
4095
4100
  try {
@@ -4419,7 +4424,7 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
4419
4424
  function generateHelpText(settings) {
4420
4425
  const bin = settings.dustCommand;
4421
4426
  return dedent`
4422
- 💨 dust - Flow state for AI coding agents.
4427
+ dust - Flow state for AI coding agents.
4423
4428
 
4424
4429
  Usage: ${bin} <command> [options]
4425
4430
 
@@ -5243,6 +5248,9 @@ var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes(" "));
5243
5248
  function isHelpRequest(command) {
5244
5249
  return !command || command === "help" || command === "--help" || command === "-h";
5245
5250
  }
5251
+ function isVersionRequest(command) {
5252
+ return command === "--version" || command === "-v";
5253
+ }
5246
5254
  function isValidCommand(command) {
5247
5255
  return command in commandRegistry;
5248
5256
  }
@@ -5262,6 +5270,10 @@ async function main(options) {
5262
5270
  const { commandArguments, context, fileSystem, glob, directoryFileSorter } = options;
5263
5271
  const settings = await loadSettings(context.cwd, fileSystem);
5264
5272
  const helpText = generateHelpText(settings);
5273
+ if (isVersionRequest(commandArguments[0])) {
5274
+ context.stdout(DUST_VERSION);
5275
+ return { exitCode: 0 };
5276
+ }
5265
5277
  if (isHelpRequest(commandArguments[0])) {
5266
5278
  context.stdout(helpText);
5267
5279
  return { exitCode: 0 };
@@ -5350,4 +5362,4 @@ await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFil
5350
5362
  process.stdout.write(message);
5351
5363
  },
5352
5364
  error: console.error
5353
- });
5365
+ });
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Filesystem abstraction types used across the codebase.
3
+ */
4
+ export interface WriteOptions {
5
+ flag?: 'w' | 'wx';
6
+ }
7
+ export interface ReadableFileSystem {
8
+ exists: (path: string) => boolean;
9
+ readFile: (path: string) => Promise<string>;
10
+ readdir: (path: string) => Promise<string[]>;
11
+ isDirectory: (path: string) => boolean;
12
+ }
13
+ export interface FileSystem extends ReadableFileSystem {
14
+ writeFile: (path: string, content: string, options?: WriteOptions) => Promise<void>;
15
+ mkdir: (path: string, options?: {
16
+ recursive?: boolean;
17
+ }) => Promise<void>;
18
+ chmod: (path: string, mode: number) => Promise<void>;
19
+ getFileCreationTime: (path: string) => number;
20
+ rename: (oldPath: string, newPath: string) => Promise<void>;
21
+ }
22
+ export interface GlobScanner {
23
+ scan: (dir: string) => AsyncIterable<string>;
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.61",
3
+ "version": "0.1.63",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,13 @@
22
22
  "import": "./dist/artifacts.js",
23
23
  "types": "./dist/artifacts/index.d.ts"
24
24
  },
25
+ "./audits": {
26
+ "import": "./dist/audits.js",
27
+ "types": "./dist/audits/index.d.ts"
28
+ },
29
+ "./filesystem": {
30
+ "types": "./dist/filesystem/types.d.ts"
31
+ },
25
32
  "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
26
33
  },
27
34
  "files": [
@@ -42,7 +49,7 @@
42
49
  "author": "joshski",
43
50
  "license": "MIT",
44
51
  "scripts": {
45
- "build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && bun build lib/logging/index.ts --target node --outfile dist/logging.js && bun build lib/agents/detection.ts --target node --outfile dist/agents.js && bun build lib/artifacts/index.ts --target node --outfile dist/artifacts.js && bunx tsc --project tsconfig.build.json",
52
+ "build": "bun run scripts/build.ts",
46
53
  "test": "vitest run",
47
54
  "test:coverage": "vitest run --coverage",
48
55
  "eval": "bun run ./evals/run.ts"