@joshski/dust 0.1.64 → 0.1.67

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.
@@ -0,0 +1,58 @@
1
+ language js
2
+
3
+ or {
4
+ `ctx` where {
5
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'ctx'. Use 'context' instead.")
6
+ },
7
+ `deps` where {
8
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'deps'. Use 'dependencies' instead.")
9
+ },
10
+ `fs` where {
11
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'fs'. Use 'fileSystem' instead.")
12
+ },
13
+ `args` where {
14
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'args'. Use 'arguments' instead.")
15
+ },
16
+ `req` where {
17
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'req'. Use 'request' instead.")
18
+ },
19
+ `res` where {
20
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'res'. Use 'response' instead.")
21
+ },
22
+ `err` where {
23
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'err'. Use 'error' instead.")
24
+ },
25
+ `cb` where {
26
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'cb'. Use 'callback' instead.")
27
+ },
28
+ `fn` where {
29
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'fn'. Use a descriptive name instead.")
30
+ },
31
+ `opts` where {
32
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'opts'. Use 'options' instead.")
33
+ },
34
+ `params` where {
35
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'params'. Use 'parameters' instead.")
36
+ },
37
+ `obj` where {
38
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'obj'. Use a descriptive name instead.")
39
+ },
40
+ `val` where {
41
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'val'. Use 'value' instead.")
42
+ },
43
+ `idx` where {
44
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'idx'. Use 'index' instead.")
45
+ },
46
+ `len` where {
47
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'len'. Use 'length' instead.")
48
+ },
49
+ `tmp` where {
50
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'tmp'. Use a descriptive name instead.")
51
+ },
52
+ `str` where {
53
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'str'. Use 'string' or a descriptive name instead.")
54
+ },
55
+ `num` where {
56
+ register_diagnostic(span=$match, message="Avoid abbreviated name 'num'. Use 'number' or a descriptive name instead.")
57
+ }
58
+ }
package/dist/artifacts.js CHANGED
@@ -302,19 +302,53 @@ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
302
302
  function titleToFilename(title) {
303
303
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
304
304
  }
305
- var WORKFLOW_TASK_TYPES = [
306
- { type: "refine", prefix: "Refine Idea: " },
307
- { type: "decompose-idea", prefix: "Decompose Idea: " },
308
- { type: "shelve", prefix: "Shelve Idea: " }
305
+ var WORKFLOW_SECTION_HEADINGS = [
306
+ { type: "refine", heading: "Refines Idea" },
307
+ { type: "decompose-idea", heading: "Decomposes Idea" },
308
+ { type: "shelve", heading: "Shelves Idea" }
309
309
  ];
310
+ function extractIdeaSlugFromSection(content, sectionHeading) {
311
+ const lines = content.split(`
312
+ `);
313
+ let inSection = false;
314
+ for (const line of lines) {
315
+ if (line.startsWith("## ")) {
316
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
317
+ continue;
318
+ }
319
+ if (!inSection)
320
+ continue;
321
+ if (line.startsWith("# "))
322
+ break;
323
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
324
+ if (linkMatch) {
325
+ const target = linkMatch[2];
326
+ const slugMatch = target.match(/([^/]+)\.md$/);
327
+ if (slugMatch) {
328
+ return slugMatch[1];
329
+ }
330
+ }
331
+ }
332
+ return null;
333
+ }
310
334
  async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
311
- const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
312
- for (const { type, prefix } of WORKFLOW_TASK_TYPES) {
313
- const filename = titleToFilename(`${prefix}${ideaTitle}`);
314
- const filePath = `${dustPath}/tasks/${filename}`;
315
- if (fileSystem.exists(filePath)) {
316
- const taskSlug = filename.replace(/\.md$/, "");
317
- return { type, ideaSlug, taskSlug };
335
+ const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
336
+ if (!fileSystem.exists(ideaPath)) {
337
+ throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
338
+ }
339
+ const tasksPath = `${dustPath}/tasks`;
340
+ if (!fileSystem.exists(tasksPath)) {
341
+ return null;
342
+ }
343
+ const files = await fileSystem.readdir(tasksPath);
344
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
345
+ const content = await fileSystem.readFile(`${tasksPath}/${file}`);
346
+ for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
347
+ const linkedSlug = extractIdeaSlugFromSection(content, heading);
348
+ if (linkedSlug === ideaSlug) {
349
+ const taskSlug = file.replace(/\.md$/, "");
350
+ return { type, ideaSlug, taskSlug };
351
+ }
318
352
  }
319
353
  }
320
354
  return null;
@@ -342,22 +376,26 @@ ${sections.join(`
342
376
  `)}
343
377
  `;
344
378
  }
345
- function renderTask(title, openingSentence, definitionOfDone, options) {
379
+ function renderIdeaSection(ideaSection) {
380
+ return `## ${ideaSection.heading}
381
+
382
+ - [${ideaSection.ideaTitle}](../ideas/${ideaSection.ideaSlug}.md)
383
+ `;
384
+ }
385
+ function renderTask(title, openingSentence, definitionOfDone, ideaSection, options) {
346
386
  const descriptionParagraph = options?.description !== undefined ? `
347
387
  ${options.description}
348
388
  ` : "";
349
389
  const resolvedSection = options?.resolvedQuestions && options.resolvedQuestions.length > 0 ? `
350
390
  ${renderResolvedQuestions(options.resolvedQuestions)}
351
391
  ` : "";
392
+ const ideaSectionContent = `
393
+ ${renderIdeaSection(ideaSection)}
394
+ `;
352
395
  return `# ${title}
353
396
 
354
397
  ${openingSentence}
355
- ${descriptionParagraph}${resolvedSection}
356
- ## Principles
357
-
358
- (none)
359
-
360
- ## Blocked By
398
+ ${descriptionParagraph}${resolvedSection}${ideaSectionContent}## Blocked By
361
399
 
362
400
  (none)
363
401
 
@@ -367,13 +405,17 @@ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
367
405
  `)}
368
406
  `;
369
407
  }
370
- async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, taskOptions) {
408
+ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, ideaSectionHeading, taskOptions) {
371
409
  const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
372
410
  const taskTitle = `${prefix}${ideaTitle}`;
373
411
  const filename = titleToFilename(taskTitle);
374
412
  const filePath = `${dustPath}/tasks/${filename}`;
375
413
  const openingSentence = openingSentenceTemplate(ideaTitle);
376
- const content = renderTask(taskTitle, openingSentence, definitionOfDone, taskOptions);
414
+ const ideaSection = { heading: ideaSectionHeading, ideaTitle, ideaSlug };
415
+ const content = renderTask(taskTitle, openingSentence, definitionOfDone, ideaSection, {
416
+ description: taskOptions?.description,
417
+ resolvedQuestions: taskOptions?.resolvedQuestions
418
+ });
377
419
  await fileSystem.writeFile(filePath, content);
378
420
  return { filePath };
379
421
  }
@@ -383,20 +425,20 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
383
425
  "Open questions are added for any ambiguous or underspecified aspects",
384
426
  "Open questions follow the required heading format and focus on high-value decisions",
385
427
  "Idea file is updated with findings"
386
- ], { description });
428
+ ], "Refines Idea", { description });
387
429
  }
388
430
  async function decomposeIdea(fileSystem, dustPath, options) {
389
431
  return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/principles/\` to link relevant principles and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
390
432
  "One or more new tasks are created in .dust/tasks/",
391
433
  "Task's Principles section links to relevant principles from .dust/principles/",
392
434
  "The original idea is deleted or updated to reflect remaining scope"
393
- ], {
435
+ ], "Decomposes Idea", {
394
436
  description: options.description,
395
437
  resolvedQuestions: options.openQuestionResponses
396
438
  });
397
439
  }
398
440
  async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
399
- return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], { description });
441
+ return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], "Shelves Idea", { description });
400
442
  }
401
443
  async function createCaptureIdeaTask(fileSystem, dustPath, options) {
402
444
  const { title, description, buildItNow } = options;
@@ -418,10 +460,6 @@ Research this idea thoroughly, then create one or more narrowly-scoped task file
418
460
 
419
461
  ${description}
420
462
 
421
- ## Principles
422
-
423
- (none)
424
-
425
463
  ## Blocked By
426
464
 
427
465
  (none)
@@ -438,28 +476,22 @@ ${description}
438
476
  const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
439
477
  const filename = titleToFilename(taskTitle);
440
478
  const filePath = `${dustPath}/tasks/${filename}`;
441
- const ideaFilename = titleToFilename(title);
442
- const ideaPath = `.dust/ideas/${ideaFilename}`;
443
479
  const content = `# ${taskTitle}
444
480
 
445
- Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context.
481
+ Research this idea thoroughly, then create one or more idea files in \`.dust/ideas/\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context.
446
482
 
447
483
  ## Idea Description
448
484
 
449
485
  ${description}
450
486
 
451
- ## Principles
452
-
453
- (none)
454
-
455
487
  ## Blocked By
456
488
 
457
489
  (none)
458
490
 
459
491
  ## Definition of Done
460
492
 
461
- - [ ] Idea file exists at ${ideaPath}
462
- - [ ] Idea file has an H1 title matching "${title}"
493
+ - [ ] One or more idea files are created in \`.dust/ideas/\`
494
+ - [ ] Each idea file has an H1 title matching its content
463
495
  - [ ] Idea includes relevant context from codebase exploration
464
496
  - [ ] Open questions are added for any ambiguous or underspecified aspects
465
497
  - [ ] Open questions follow the required heading format and focus on high-value decisions
@@ -4,9 +4,10 @@
4
4
  * Users can override any of these by placing a file with the same name
5
5
  * in .dust/config/audits/.
6
6
  */
7
- export interface StockAudit {
7
+ interface StockAudit {
8
8
  name: string;
9
9
  description: string;
10
10
  template: string;
11
11
  }
12
12
  export declare function loadStockAudits(): StockAudit[];
13
+ export {};
package/dist/audits.js CHANGED
@@ -63,11 +63,50 @@ function dedent(strings, ...values) {
63
63
  }
64
64
 
65
65
  // lib/audits/stock-audits.ts
66
+ var ideasHint = "Review existing ideas in `./.dust/ideas/` to understand what has been proposed or considered historically, then create new idea files in `./.dust/ideas/` for any issues you identify, avoiding duplication.";
67
+ function componentReuse() {
68
+ return dedent`
69
+ # Component Reuse
70
+
71
+ Find repeated patterns and code that could be extracted into reusable components.
72
+
73
+ ${ideasHint}
74
+
75
+ ## Scope
76
+
77
+ Focus on these areas:
78
+
79
+ 1. **Repeated patterns** - Similar code blocks that appear multiple times
80
+ 2. **Copy-pasted code** - Near-identical logic across different files
81
+ 3. **Parallel structures** - Code that handles similar cases with minor variations
82
+ 4. **Extraction opportunities** - Logic that could be unified without forcing unrelated concepts together
83
+
84
+ ## Principles
85
+
86
+ - [Reasonably DRY](../principles/reasonably-dry.md)
87
+ - [Decoupled Code](../principles/decoupled-code.md)
88
+ - [Maintainable Codebase](../principles/maintainable-codebase.md)
89
+
90
+ ## Blocked By
91
+
92
+ (none)
93
+
94
+ ## Definition of Done
95
+
96
+ - [ ] Searched for repeated patterns across the codebase
97
+ - [ ] Identified copy-pasted or near-duplicate code
98
+ - [ ] Evaluated each case for whether extraction would be beneficial
99
+ - [ ] Considered whether similar code serves different purposes that may evolve independently
100
+ - [ ] Proposed ideas only for extractions where duplication is truly about the same concept
101
+ `;
102
+ }
66
103
  function agentDeveloperExperience() {
67
104
  return dedent`
68
105
  # Agent Developer Experience
69
106
 
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.
107
+ Review the codebase to ensure agents have everything they need to operate effectively.
108
+
109
+ ${ideasHint}
71
110
 
72
111
  ## Scope
73
112
 
@@ -100,7 +139,9 @@ function deadCode() {
100
139
  return dedent`
101
140
  # Dead Code
102
141
 
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.
142
+ Find and remove unused code to improve maintainability and reduce bundle size.
143
+
144
+ ${ideasHint}
104
145
 
105
146
  ## Scope
106
147
 
@@ -135,7 +176,9 @@ function factsVerification() {
135
176
  return dedent`
136
177
  # Facts Verification
137
178
 
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.
179
+ Review \`.dust/facts/\` to ensure documented facts match current reality.
180
+
181
+ ${ideasHint}
139
182
 
140
183
  ## Scope
141
184
 
@@ -168,7 +211,9 @@ function ideasFromCommits() {
168
211
  return dedent`
169
212
  # Ideas from Commits
170
213
 
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.
214
+ Review recent commit history to identify follow-up improvement ideas.
215
+
216
+ ${ideasHint}
172
217
 
173
218
  ## Scope
174
219
 
@@ -200,7 +245,9 @@ function ideasFromPrinciples() {
200
245
  return dedent`
201
246
  # Ideas from Principles
202
247
 
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.
248
+ Review \`.dust/principles/\` to generate new improvement ideas.
249
+
250
+ ${ideasHint}
204
251
 
205
252
  ## Scope
206
253
 
@@ -231,7 +278,9 @@ function performanceReview() {
231
278
  return dedent`
232
279
  # Performance Review
233
280
 
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.
281
+ Review the application for performance issues and optimization opportunities.
282
+
283
+ ${ideasHint}
235
284
 
236
285
  ## Scope
237
286
 
@@ -264,7 +313,9 @@ function securityReview() {
264
313
  return dedent`
265
314
  # Security Review
266
315
 
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.
316
+ Review the codebase for common security vulnerabilities and misconfigurations.
317
+
318
+ ${ideasHint}
268
319
 
269
320
  ## Scope
270
321
 
@@ -299,7 +350,9 @@ function staleIdeas() {
299
350
  return dedent`
300
351
  # Stale Ideas
301
352
 
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.
353
+ Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
354
+
355
+ ${ideasHint}
303
356
 
304
357
  ## Scope
305
358
 
@@ -331,7 +384,9 @@ function testCoverage() {
331
384
  return dedent`
332
385
  # Test Coverage
333
386
 
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.
387
+ Identify untested code paths and areas that need additional test coverage.
388
+
389
+ ${ideasHint}
335
390
 
336
391
  ## Scope
337
392
 
@@ -361,6 +416,7 @@ function testCoverage() {
361
416
  }
362
417
  var stockAuditFunctions = {
363
418
  "agent-developer-experience": agentDeveloperExperience,
419
+ "component-reuse": componentReuse,
364
420
  "dead-code": deadCode,
365
421
  "facts-verification": factsVerification,
366
422
  "ideas-from-commits": ideasFromCommits,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Biome path export
3
+ *
4
+ * Provides the path to the biome directory containing custom GritQL lint rules.
5
+ * Downstream users can reference this path in their biome.json plugins array.
6
+ */
7
+ /**
8
+ * Absolute path to the biome directory containing custom GritQL lint rules.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { biomePath } from "@joshski/dust/biome";
13
+ * // Returns: "/path/to/node_modules/@joshski/dust/biome"
14
+ * ```
15
+ */
16
+ export declare const biomePath: string;
package/dist/biome.js ADDED
@@ -0,0 +1,8 @@
1
+ // lib/biome/index.ts
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ var currentDir = dirname(fileURLToPath(import.meta.url));
5
+ var biomePath = join(currentDir, "..", "biome");
6
+ export {
7
+ biomePath
8
+ };
@@ -2,7 +2,7 @@
2
2
  * Common types for CLI commands
3
3
  */
4
4
  import type { FileSystem, GlobScanner } from '../filesystem/types';
5
- export type { FileSystem, GlobScanner, ReadableFileSystem, WriteOptions, } from '../filesystem/types';
5
+ export type { FileSystem, GlobScanner, ReadableFileSystem, } from '../filesystem/types';
6
6
  export interface CommandContext {
7
7
  cwd: string;
8
8
  stdout: (message: string) => void;
package/dist/dust.js CHANGED
@@ -275,7 +275,25 @@ async function loadSettings(cwd, fileSystem) {
275
275
  }
276
276
 
277
277
  // lib/version.ts
278
- var DUST_VERSION = "0.1.64";
278
+ var DUST_VERSION = "0.1.67";
279
+
280
+ // lib/session.ts
281
+ var DUST_UNATTENDED = "DUST_UNATTENDED";
282
+ var DUST_SKIP_AGENT = "DUST_SKIP_AGENT";
283
+ var DUST_REPOSITORY_ID = "DUST_REPOSITORY_ID";
284
+ function isUnattended(env = process.env) {
285
+ return !!env[DUST_UNATTENDED];
286
+ }
287
+ function buildUnattendedEnv(options) {
288
+ const env = {
289
+ [DUST_UNATTENDED]: "1",
290
+ [DUST_SKIP_AGENT]: "1"
291
+ };
292
+ if (options?.repositoryId) {
293
+ env[DUST_REPOSITORY_ID] = options.repositoryId;
294
+ }
295
+ return env;
296
+ }
279
297
 
280
298
  // lib/cli/dedent.ts
281
299
  function dedent(strings, ...values) {
@@ -524,7 +542,7 @@ ${vars.agentInstructions}` : "";
524
542
  }
525
543
  async function agent(dependencies, env = process.env) {
526
544
  const { context, fileSystem, settings } = dependencies;
527
- if (env.DUST_SKIP_AGENT === "1") {
545
+ if (env[DUST_SKIP_AGENT] === "1") {
528
546
  context.stdout("You're running in an automated loop - proceeding to implement the assigned task.");
529
547
  return { exitCode: 0 };
530
548
  }
@@ -590,11 +608,50 @@ function extractOpeningSentence(content) {
590
608
  }
591
609
 
592
610
  // lib/audits/stock-audits.ts
611
+ var ideasHint = "Review existing ideas in `./.dust/ideas/` to understand what has been proposed or considered historically, then create new idea files in `./.dust/ideas/` for any issues you identify, avoiding duplication.";
612
+ function componentReuse() {
613
+ return dedent`
614
+ # Component Reuse
615
+
616
+ Find repeated patterns and code that could be extracted into reusable components.
617
+
618
+ ${ideasHint}
619
+
620
+ ## Scope
621
+
622
+ Focus on these areas:
623
+
624
+ 1. **Repeated patterns** - Similar code blocks that appear multiple times
625
+ 2. **Copy-pasted code** - Near-identical logic across different files
626
+ 3. **Parallel structures** - Code that handles similar cases with minor variations
627
+ 4. **Extraction opportunities** - Logic that could be unified without forcing unrelated concepts together
628
+
629
+ ## Principles
630
+
631
+ - [Reasonably DRY](../principles/reasonably-dry.md)
632
+ - [Decoupled Code](../principles/decoupled-code.md)
633
+ - [Maintainable Codebase](../principles/maintainable-codebase.md)
634
+
635
+ ## Blocked By
636
+
637
+ (none)
638
+
639
+ ## Definition of Done
640
+
641
+ - [ ] Searched for repeated patterns across the codebase
642
+ - [ ] Identified copy-pasted or near-duplicate code
643
+ - [ ] Evaluated each case for whether extraction would be beneficial
644
+ - [ ] Considered whether similar code serves different purposes that may evolve independently
645
+ - [ ] Proposed ideas only for extractions where duplication is truly about the same concept
646
+ `;
647
+ }
593
648
  function agentDeveloperExperience() {
594
649
  return dedent`
595
650
  # Agent Developer Experience
596
651
 
597
- 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.
652
+ Review the codebase to ensure agents have everything they need to operate effectively.
653
+
654
+ ${ideasHint}
598
655
 
599
656
  ## Scope
600
657
 
@@ -627,7 +684,9 @@ function deadCode() {
627
684
  return dedent`
628
685
  # Dead Code
629
686
 
630
- 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.
687
+ Find and remove unused code to improve maintainability and reduce bundle size.
688
+
689
+ ${ideasHint}
631
690
 
632
691
  ## Scope
633
692
 
@@ -662,7 +721,9 @@ function factsVerification() {
662
721
  return dedent`
663
722
  # Facts Verification
664
723
 
665
- 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.
724
+ Review \`.dust/facts/\` to ensure documented facts match current reality.
725
+
726
+ ${ideasHint}
666
727
 
667
728
  ## Scope
668
729
 
@@ -695,7 +756,9 @@ function ideasFromCommits() {
695
756
  return dedent`
696
757
  # Ideas from Commits
697
758
 
698
- 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.
759
+ Review recent commit history to identify follow-up improvement ideas.
760
+
761
+ ${ideasHint}
699
762
 
700
763
  ## Scope
701
764
 
@@ -727,7 +790,9 @@ function ideasFromPrinciples() {
727
790
  return dedent`
728
791
  # Ideas from Principles
729
792
 
730
- 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.
793
+ Review \`.dust/principles/\` to generate new improvement ideas.
794
+
795
+ ${ideasHint}
731
796
 
732
797
  ## Scope
733
798
 
@@ -758,7 +823,9 @@ function performanceReview() {
758
823
  return dedent`
759
824
  # Performance Review
760
825
 
761
- 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.
826
+ Review the application for performance issues and optimization opportunities.
827
+
828
+ ${ideasHint}
762
829
 
763
830
  ## Scope
764
831
 
@@ -791,7 +858,9 @@ function securityReview() {
791
858
  return dedent`
792
859
  # Security Review
793
860
 
794
- 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.
861
+ Review the codebase for common security vulnerabilities and misconfigurations.
862
+
863
+ ${ideasHint}
795
864
 
796
865
  ## Scope
797
866
 
@@ -826,7 +895,9 @@ function staleIdeas() {
826
895
  return dedent`
827
896
  # Stale Ideas
828
897
 
829
- 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.
898
+ Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
899
+
900
+ ${ideasHint}
830
901
 
831
902
  ## Scope
832
903
 
@@ -858,7 +929,9 @@ function testCoverage() {
858
929
  return dedent`
859
930
  # Test Coverage
860
931
 
861
- 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.
932
+ Identify untested code paths and areas that need additional test coverage.
933
+
934
+ ${ideasHint}
862
935
 
863
936
  ## Scope
864
937
 
@@ -888,6 +961,7 @@ function testCoverage() {
888
961
  }
889
962
  var stockAuditFunctions = {
890
963
  "agent-developer-experience": agentDeveloperExperience,
964
+ "component-reuse": componentReuse,
891
965
  "dead-code": deadCode,
892
966
  "facts-verification": factsVerification,
893
967
  "ideas-from-commits": ideasFromCommits,
@@ -2133,13 +2207,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
2133
2207
  logger = log,
2134
2208
  repositoryId
2135
2209
  } = options;
2136
- const baseEnv = {
2137
- DUST_UNATTENDED: "1",
2138
- DUST_SKIP_AGENT: "1"
2139
- };
2140
- if (repositoryId) {
2141
- baseEnv.DUST_REPOSITORY_ID = repositoryId;
2142
- }
2210
+ const baseEnv = buildUnattendedEnv({ repositoryId });
2143
2211
  log("syncing with remote");
2144
2212
  onLoopEvent({ type: "loop.syncing" });
2145
2213
  const pullResult = await gitPull(context.cwd, spawn);
@@ -2263,6 +2331,10 @@ function parseMaxIterations(commandArguments) {
2263
2331
  async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
2264
2332
  enableFileLogs("loop");
2265
2333
  const { context, settings } = dependencies;
2334
+ if (isUnattended()) {
2335
+ context.stderr("dust loop cannot run inside an unattended session (DUST_UNATTENDED is set)");
2336
+ return { exitCode: 1 };
2337
+ }
2266
2338
  const { postEvent } = loopDependencies;
2267
2339
  const maxIterations = parseMaxIterations(dependencies.arguments);
2268
2340
  const eventsUrl = settings.eventsUrl;
@@ -3446,9 +3518,13 @@ async function resolveToken(authDeps, context) {
3446
3518
  return null;
3447
3519
  }
3448
3520
  }
3449
- async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
3521
+ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDependencies()) {
3450
3522
  enableFileLogs("bucket");
3451
3523
  const { context, fileSystem } = dependencies;
3524
+ if (isUnattended()) {
3525
+ context.stderr("dust bucket cannot run inside an unattended session (DUST_UNATTENDED is set)");
3526
+ return { exitCode: 1 };
3527
+ }
3452
3528
  const token = await resolveToken(bucketDeps.auth, context);
3453
3529
  if (!token) {
3454
3530
  return { exitCode: 1 };
@@ -3740,11 +3816,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3740
3816
  import { join as join8 } from "node:path";
3741
3817
 
3742
3818
  // lib/lint/validators/content-validator.ts
3743
- var REQUIRED_HEADINGS = [
3744
- "## Principles",
3745
- "## Blocked By",
3746
- "## Definition of Done"
3747
- ];
3819
+ var REQUIRED_HEADINGS = ["## Blocked By", "## Definition of Done"];
3748
3820
  var MAX_OPENING_SENTENCE_LENGTH = 150;
3749
3821
  var NON_IMPERATIVE_STARTERS = new Set([
3750
3822
  "the",
@@ -3912,6 +3984,11 @@ function validateTitleFilenameMatch(filePath, content) {
3912
3984
  }
3913
3985
 
3914
3986
  // lib/lint/validators/idea-validator.ts
3987
+ var WORKFLOW_PREFIX_TO_SECTION = {
3988
+ "Refine Idea: ": "Refines Idea",
3989
+ "Decompose Idea: ": "Decomposes Idea",
3990
+ "Shelve Idea: ": "Shelves Idea"
3991
+ };
3915
3992
  function validateIdeaOpenQuestions(filePath, content) {
3916
3993
  const violations = [];
3917
3994
  const lines = content.split(`
@@ -4025,6 +4102,114 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
4025
4102
  }
4026
4103
  return null;
4027
4104
  }
4105
+ function extractSectionContent(content, sectionHeading) {
4106
+ const lines = content.split(`
4107
+ `);
4108
+ let inSection = false;
4109
+ let sectionContent = "";
4110
+ let startLine = 0;
4111
+ for (let i = 0;i < lines.length; i++) {
4112
+ const line = lines[i];
4113
+ if (line.startsWith("## ")) {
4114
+ if (inSection)
4115
+ break;
4116
+ if (line.trimEnd() === `## ${sectionHeading}`) {
4117
+ inSection = true;
4118
+ startLine = i + 1;
4119
+ }
4120
+ continue;
4121
+ }
4122
+ if (line.startsWith("# ") && inSection)
4123
+ break;
4124
+ if (inSection) {
4125
+ sectionContent += `${line}
4126
+ `;
4127
+ }
4128
+ }
4129
+ if (!inSection)
4130
+ return null;
4131
+ return { content: sectionContent, startLine };
4132
+ }
4133
+ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSystem) {
4134
+ const violations = [];
4135
+ const title = extractTitle(content);
4136
+ if (!title)
4137
+ return violations;
4138
+ let matchedPrefix = null;
4139
+ for (const prefix of IDEA_TRANSITION_PREFIXES) {
4140
+ if (title.startsWith(prefix)) {
4141
+ matchedPrefix = prefix;
4142
+ break;
4143
+ }
4144
+ }
4145
+ if (!matchedPrefix)
4146
+ return violations;
4147
+ const expectedHeading = WORKFLOW_PREFIX_TO_SECTION[matchedPrefix];
4148
+ const section = extractSectionContent(content, expectedHeading);
4149
+ if (!section) {
4150
+ violations.push({
4151
+ file: filePath,
4152
+ message: `Workflow task with "${matchedPrefix.trim()}" prefix is missing required "## ${expectedHeading}" section. Add a section with a link to the idea file, e.g.:
4153
+
4154
+ ## ${expectedHeading}
4155
+
4156
+ - [Idea Title](../ideas/idea-slug.md)`
4157
+ });
4158
+ return violations;
4159
+ }
4160
+ const linkRegex = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
4161
+ const links = [];
4162
+ const sectionLines = section.content.split(`
4163
+ `);
4164
+ for (let i = 0;i < sectionLines.length; i++) {
4165
+ const line = sectionLines[i];
4166
+ let match = linkRegex.exec(line);
4167
+ while (match !== null) {
4168
+ links.push({
4169
+ text: match[1],
4170
+ target: match[2],
4171
+ line: section.startLine + i + 1
4172
+ });
4173
+ match = linkRegex.exec(line);
4174
+ }
4175
+ }
4176
+ if (links.length === 0) {
4177
+ violations.push({
4178
+ file: filePath,
4179
+ message: `"## ${expectedHeading}" section contains no link. Add a markdown link to the idea file, e.g.:
4180
+
4181
+ - [Idea Title](../ideas/idea-slug.md)`,
4182
+ line: section.startLine
4183
+ });
4184
+ return violations;
4185
+ }
4186
+ const ideaLinks = links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
4187
+ if (ideaLinks.length === 0) {
4188
+ violations.push({
4189
+ file: filePath,
4190
+ message: `"## ${expectedHeading}" section contains no link to an idea file. Links must point to a file in ../ideas/, e.g.:
4191
+
4192
+ - [Idea Title](../ideas/idea-slug.md)`,
4193
+ line: section.startLine
4194
+ });
4195
+ return violations;
4196
+ }
4197
+ for (const link of ideaLinks) {
4198
+ const slugMatch = link.target.match(/([^/]+)\.md$/);
4199
+ if (!slugMatch)
4200
+ continue;
4201
+ const ideaSlug = slugMatch[1];
4202
+ const ideaFilePath = `${ideasPath}/${ideaSlug}.md`;
4203
+ if (!fileSystem.exists(ideaFilePath)) {
4204
+ violations.push({
4205
+ file: filePath,
4206
+ message: `Link to idea "${link.text}" points to non-existent file: ${ideaSlug}.md. Either create the idea file at ideas/${ideaSlug}.md or update the link to point to an existing idea.`,
4207
+ line: link.line
4208
+ });
4209
+ }
4210
+ }
4211
+ return violations;
4212
+ }
4028
4213
 
4029
4214
  // lib/lint/validators/link-validator.ts
4030
4215
  import { dirname as dirname3, resolve } from "node:path";
@@ -4428,6 +4613,7 @@ async function lintMarkdown(dependencies) {
4428
4613
  if (ideaTransitionViolation) {
4429
4614
  violations.push(ideaTransitionViolation);
4430
4615
  }
4616
+ violations.push(...validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSystem));
4431
4617
  }
4432
4618
  }
4433
4619
  const principlesPath = `${dustPath}/principles`;
@@ -5376,7 +5562,7 @@ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process
5376
5562
  if (agent2.type === "unknown") {
5377
5563
  return { exitCode: 0 };
5378
5564
  }
5379
- if (env.DUST_UNATTENDED) {
5565
+ if (isUnattended(env)) {
5380
5566
  const uncommittedFiles = await getUncommittedFiles(context.cwd, gitRunner);
5381
5567
  if (uncommittedFiles.length > 0) {
5382
5568
  context.stderr("");
@@ -5446,7 +5632,7 @@ var commandRegistry = {
5446
5632
  check,
5447
5633
  agent,
5448
5634
  audit,
5449
- bucket,
5635
+ "bucket worker": bucketWorker,
5450
5636
  "bucket asset upload": bucketAssetUpload,
5451
5637
  focus,
5452
5638
  "new task": newTask,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.64",
3
+ "version": "0.1.67",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,12 +29,17 @@
29
29
  "./filesystem": {
30
30
  "types": "./dist/filesystem/types.d.ts"
31
31
  },
32
- "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
32
+ "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs",
33
+ "./biome": {
34
+ "import": "./dist/biome.js",
35
+ "types": "./dist/biome/index.d.ts"
36
+ }
33
37
  },
34
38
  "files": [
35
39
  "dist",
36
40
  "bin",
37
- "lib/istanbul/minimal-reporter.cjs"
41
+ "lib/istanbul/minimal-reporter.cjs",
42
+ "biome"
38
43
  ],
39
44
  "repository": {
40
45
  "type": "git",
@@ -58,9 +63,8 @@
58
63
  "@biomejs/biome": "^2.3.13",
59
64
  "@types/bun": "^1.3.6",
60
65
  "@vitest/coverage-v8": "^4.0.18",
66
+ "istanbul-lib-coverage": "^3.2.2",
67
+ "istanbul-lib-report": "^3.0.1",
61
68
  "vitest": "^4.0.18"
62
- },
63
- "dependencies": {
64
- "@joshski/dust": "^0.1.49"
65
69
  }
66
70
  }