@joshski/dust 0.1.41 → 0.1.43

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 CHANGED
@@ -1,10 +1,149 @@
1
1
  #!/usr/bin/env node
2
2
  // lib/cli/run.ts
3
- import { existsSync } from "node:fs";
3
+ import { existsSync, statSync as statSync2 } from "node:fs";
4
4
  import { chmod as chmod2, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
5
5
 
6
6
  // lib/config/settings.ts
7
7
  import { join } from "node:path";
8
+ var KNOWN_SETTINGS_KEYS = new Set([
9
+ "dustCommand",
10
+ "checks",
11
+ "extraDirectories",
12
+ "installCommand",
13
+ "eventsUrl"
14
+ ]);
15
+ var KNOWN_CHECK_KEYS = new Set([
16
+ "name",
17
+ "command",
18
+ "hints",
19
+ "timeoutMilliseconds"
20
+ ]);
21
+ function validateSettingsJson(content) {
22
+ const violations = [];
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(content);
26
+ } catch (error) {
27
+ violations.push({
28
+ message: `Invalid JSON: ${error.message}`
29
+ });
30
+ return violations;
31
+ }
32
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
33
+ violations.push({
34
+ message: "settings.json must be a JSON object"
35
+ });
36
+ return violations;
37
+ }
38
+ const settings = parsed;
39
+ for (const key of Object.keys(settings)) {
40
+ if (!KNOWN_SETTINGS_KEYS.has(key)) {
41
+ violations.push({
42
+ message: `Unknown key "${key}" in settings.json. Known keys: ${[...KNOWN_SETTINGS_KEYS].sort().join(", ")}`
43
+ });
44
+ }
45
+ }
46
+ if ("checks" in settings) {
47
+ if (!Array.isArray(settings.checks)) {
48
+ violations.push({
49
+ message: '"checks" must be an array'
50
+ });
51
+ } else {
52
+ for (let i = 0;i < settings.checks.length; i++) {
53
+ const check = settings.checks[i];
54
+ const checkPath = `checks[${i}]`;
55
+ if (typeof check === "string") {
56
+ continue;
57
+ }
58
+ if (typeof check !== "object" || check === null || Array.isArray(check)) {
59
+ violations.push({
60
+ message: `${checkPath} must be a string or object`
61
+ });
62
+ continue;
63
+ }
64
+ const checkObj = check;
65
+ for (const key of Object.keys(checkObj)) {
66
+ if (!KNOWN_CHECK_KEYS.has(key)) {
67
+ violations.push({
68
+ message: `Unknown key "${key}" in ${checkPath}. Known keys: ${[...KNOWN_CHECK_KEYS].sort().join(", ")}`
69
+ });
70
+ }
71
+ }
72
+ if (!("name" in checkObj)) {
73
+ violations.push({
74
+ message: `${checkPath} is missing required field "name"`
75
+ });
76
+ } else if (typeof checkObj.name !== "string") {
77
+ violations.push({
78
+ message: `${checkPath}.name must be a string`
79
+ });
80
+ }
81
+ if (!("command" in checkObj)) {
82
+ violations.push({
83
+ message: `${checkPath} is missing required field "command"`
84
+ });
85
+ } else if (typeof checkObj.command !== "string") {
86
+ violations.push({
87
+ message: `${checkPath}.command must be a string`
88
+ });
89
+ }
90
+ if ("hints" in checkObj) {
91
+ if (!Array.isArray(checkObj.hints)) {
92
+ violations.push({
93
+ message: `${checkPath}.hints must be an array of strings`
94
+ });
95
+ } else {
96
+ for (let j = 0;j < checkObj.hints.length; j++) {
97
+ if (typeof checkObj.hints[j] !== "string") {
98
+ violations.push({
99
+ message: `${checkPath}.hints[${j}] must be a string`
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+ if ("timeoutMilliseconds" in checkObj) {
106
+ if (typeof checkObj.timeoutMilliseconds !== "number" || checkObj.timeoutMilliseconds <= 0) {
107
+ violations.push({
108
+ message: `${checkPath}.timeoutMilliseconds must be a positive number`
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ if ("extraDirectories" in settings) {
116
+ if (!Array.isArray(settings.extraDirectories)) {
117
+ violations.push({
118
+ message: '"extraDirectories" must be an array of strings'
119
+ });
120
+ } else {
121
+ for (let i = 0;i < settings.extraDirectories.length; i++) {
122
+ if (typeof settings.extraDirectories[i] !== "string") {
123
+ violations.push({
124
+ message: `extraDirectories[${i}] must be a string`
125
+ });
126
+ }
127
+ }
128
+ }
129
+ }
130
+ if ("dustCommand" in settings && typeof settings.dustCommand !== "string") {
131
+ violations.push({
132
+ message: '"dustCommand" must be a string'
133
+ });
134
+ }
135
+ if ("installCommand" in settings && typeof settings.installCommand !== "string") {
136
+ violations.push({
137
+ message: '"installCommand" must be a string'
138
+ });
139
+ }
140
+ if ("eventsUrl" in settings && typeof settings.eventsUrl !== "string") {
141
+ violations.push({
142
+ message: '"eventsUrl" must be a string'
143
+ });
144
+ }
145
+ return violations;
146
+ }
8
147
  var DEFAULT_SETTINGS = {
9
148
  dustCommand: "npx dust",
10
149
  installCommand: "npm install"
@@ -110,36 +249,18 @@ async function loadSettings(cwd, fileSystem) {
110
249
  }
111
250
  }
112
251
 
113
- // lib/cli/templates.ts
114
- import { readFileSync } from "node:fs";
115
- import { dirname, join as join2 } from "node:path";
116
- import { fileURLToPath } from "node:url";
117
- var __dirname2 = dirname(fileURLToPath(import.meta.url));
118
- var templatesDir = join2(__dirname2, "../templates");
119
- function isTruthy(value) {
120
- return value !== undefined && value !== "" && value !== "false";
121
- }
122
- function processConditionals(content, variables) {
123
- let result = content.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, varName, block) => {
124
- return isTruthy(variables[varName]) ? block : "";
125
- });
126
- result = result.replace(/\{\{#unless (\w+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (_, varName, block) => {
127
- return !isTruthy(variables[varName]) ? block : "";
128
- });
129
- return result;
130
- }
131
- function loadTemplate(name, variables = {}) {
132
- const templatePath = join2(templatesDir, `${name}.txt`);
133
- let content = readFileSync(templatePath, "utf-8");
134
- content = processConditionals(content, variables);
135
- for (const [key, value] of Object.entries(variables)) {
136
- content = content.replaceAll(`{{${key}}}`, value);
137
- }
138
- return content;
252
+ // lib/cli/dedent.ts
253
+ function dedent(strings, ...values) {
254
+ const result = strings.reduce((acc, part, index) => acc + part + (values[index] ?? ""), "");
255
+ const lines = result.split(`
256
+ `);
257
+ const indent = lines.filter((line) => line.trim()).reduce((min, line) => Math.min(min, line.match(/^\s*/)[0].length), Number.POSITIVE_INFINITY);
258
+ return lines.map((line) => line.slice(indent)).join(`
259
+ `).trim();
139
260
  }
140
261
 
141
262
  // lib/cli/commands/agent-shared.ts
142
- import { join as join4 } from "node:path";
263
+ import { join as join3 } from "node:path";
143
264
 
144
265
  // lib/agents/detection.ts
145
266
  function detectAgent(env = process.env) {
@@ -156,7 +277,7 @@ function detectAgent(env = process.env) {
156
277
  }
157
278
 
158
279
  // lib/git/hooks.ts
159
- import { join as join3 } from "node:path";
280
+ import { join as join2 } from "node:path";
160
281
  var DUST_HOOK_START = "# BEGIN DUST HOOK";
161
282
  var DUST_HOOK_END = "# END DUST HOOK";
162
283
  function generateHookContent(dustCommand) {
@@ -189,9 +310,9 @@ function removeDustSection(content) {
189
310
  `).trim();
190
311
  }
191
312
  function createHooksManager(cwd, fileSystem, settings) {
192
- const gitDir = join3(cwd, ".git");
193
- const hooksDir = join3(gitDir, "hooks");
194
- const prePushPath = join3(hooksDir, "pre-push");
313
+ const gitDir = join2(cwd, ".git");
314
+ const hooksDir = join2(gitDir, "hooks");
315
+ const prePushPath = join2(hooksDir, "pre-push");
195
316
  return {
196
317
  isGitRepo: () => fileSystem.exists(gitDir),
197
318
  isHookInstalled: async () => {
@@ -286,7 +407,7 @@ ${newHookContent}
286
407
 
287
408
  // lib/cli/commands/agent-shared.ts
288
409
  async function loadAgentInstructions(cwd, fileSystem, agentType) {
289
- const instructionsPath = join4(cwd, ".dust", "config", "agents", `${agentType}.md`);
410
+ const instructionsPath = join3(cwd, ".dust", "config", "agents", `${agentType}.md`);
290
411
  if (!fileSystem.exists(instructionsPath)) {
291
412
  return "";
292
413
  }
@@ -297,23 +418,27 @@ async function loadAgentInstructions(cwd, fileSystem, agentType) {
297
418
  return "";
298
419
  }
299
420
  }
300
- function templateVariables(settings, hooksInstalled, env = process.env) {
421
+ function templateVariables(settings, hooksInstalled, env = process.env, options) {
301
422
  const agent = detectAgent(env);
423
+ const hasIdeaFile = options?.hasIdeaFile ?? true;
302
424
  return {
303
425
  bin: settings.dustCommand,
304
426
  agentName: agent.name,
305
- hooksInstalled: hooksInstalled ? "true" : "false",
306
- isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : ""
427
+ hooksInstalled,
428
+ isClaudeCodeWeb: agent.type === "claude-code-web",
429
+ hasIdeaFile
307
430
  };
308
431
  }
309
- async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env) {
432
+ async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env, options) {
310
433
  const agent = detectAgent(env);
311
434
  const agentInstructions = await loadAgentInstructions(cwd, fileSystem, agent.type);
435
+ const hasIdeaFile = options?.hasIdeaFile ?? true;
312
436
  return {
313
437
  bin: settings.dustCommand,
314
438
  agentName: agent.name,
315
- hooksInstalled: hooksInstalled ? "true" : "false",
316
- isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : "",
439
+ hooksInstalled,
440
+ isClaudeCodeWeb: agent.type === "claude-code-web",
441
+ hasIdeaFile,
317
442
  agentInstructions
318
443
  };
319
444
  }
@@ -336,6 +461,39 @@ async function manageGitHooks(dependencies) {
336
461
  }
337
462
 
338
463
  // lib/cli/commands/agent.ts
464
+ function agentGreeting(vars) {
465
+ const instructions = vars.agentInstructions ? `
466
+ ---
467
+
468
+ ${vars.agentInstructions}` : "";
469
+ return dedent`
470
+ 🤖 Hello ${vars.agentName}, welcome to dust!
471
+
472
+ CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
473
+
474
+ Determine the user's intent and run the matching command NOW:
475
+
476
+ 1. **Pick up work from the backlog** → \`${vars.bin} pick task\`
477
+ User wants to start working. Examples: "work", "go", "pick a task", "what's next?"
478
+
479
+ 2. **Implement a specific task** → \`${vars.bin} focus "<task name>"\`
480
+ User mentions a particular task by name. Examples: "implement the auth task", "work on caching"
481
+
482
+ 3. **Capture a new task** → \`${vars.bin} new task\`
483
+ User has concrete work to add. Keywords: "task: ..." or "add a task ..."
484
+
485
+ 4. **Capture a new goal** → \`${vars.bin} new goal\`
486
+ User has a higher-level objective to add. Keywords: "goal: ..." or "add a goal ..."
487
+
488
+ 5. **Capture a vague idea** → \`${vars.bin} new idea\`
489
+ User has a rough idea that might become work later. Keywords: "idea: ..." or "add an idea ..."
490
+
491
+ 6. **Unclear** → \`${vars.bin} help\`
492
+ If none of the above clearly apply, run this to see all available commands.
493
+
494
+ Do NOT proceed without running one of these commands.${instructions}
495
+ `;
496
+ }
339
497
  async function agent(dependencies, env = process.env) {
340
498
  const { context, fileSystem, settings } = dependencies;
341
499
  if (env.DUST_SKIP_AGENT === "1") {
@@ -343,15 +501,13 @@ async function agent(dependencies, env = process.env) {
343
501
  return { exitCode: 0 };
344
502
  }
345
503
  const hooksInstalled = await manageGitHooks(dependencies);
346
- const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled);
347
- context.stdout(loadTemplate("agent-greeting", vars));
504
+ const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled, env);
505
+ context.stdout(agentGreeting(vars));
348
506
  return { exitCode: 0 };
349
507
  }
350
508
 
351
509
  // lib/cli/commands/audit.ts
352
- import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
353
- import { basename, dirname as dirname2, join as join5 } from "node:path";
354
- import { fileURLToPath as fileURLToPath2 } from "node:url";
510
+ import { basename } from "node:path";
355
511
 
356
512
  // lib/markdown/markdown-utilities.ts
357
513
  function extractTitle(content) {
@@ -405,6 +561,286 @@ function extractOpeningSentence(content) {
405
561
  return sentenceMatch[1];
406
562
  }
407
563
 
564
+ // lib/audits/stock-audits.ts
565
+ function agentDeveloperExperience() {
566
+ return dedent`
567
+ # Agent Developer Experience
568
+
569
+ 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.
570
+
571
+ ## Scope
572
+
573
+ Focus on these areas:
574
+
575
+ 1. **Context window efficiency** - Are files small and well-organized?
576
+ 2. **Test coverage** - Can agents verify correctness through tests?
577
+ 3. **Feedback loop speed** - How fast are checks and tests?
578
+ 4. **Debugging tools** - Can agents diagnose issues without trial and error?
579
+ 5. **Structured logging** - Is system behavior observable through logs?
580
+
581
+ ## Blocked By
582
+
583
+ (none)
584
+
585
+ ## Definition of Done
586
+
587
+ - [ ] Reviewed file sizes and organization for context window fit
588
+ - [ ] Verified test coverage is sufficient for agent verification
589
+ - [ ] Measured feedback loop speed (time from change to check result)
590
+ - [ ] Confirmed debugging tools and structured logging are in place
591
+ - [ ] Proposed ideas for any improvements identified
592
+ `;
593
+ }
594
+ function deadCode() {
595
+ return dedent`
596
+ # Dead Code
597
+
598
+ 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.
599
+
600
+ ## Scope
601
+
602
+ Focus on these areas:
603
+
604
+ 1. **Unused exports** - Functions, classes, constants that are never imported
605
+ 2. **Unreachable code** - Code after return statements, impossible conditions
606
+ 3. **Orphaned files** - Files that are not imported anywhere
607
+ 4. **Unused dependencies** - Packages in package.json not used in code
608
+ 5. **Commented-out code** - Old code left in comments
609
+
610
+ ## Blocked By
611
+
612
+ (none)
613
+
614
+ ## Definition of Done
615
+
616
+ - [ ] Ran static analysis tools to find unused exports
617
+ - [ ] Identified files with no incoming imports
618
+ - [ ] Listed unused dependencies
619
+ - [ ] Reviewed commented-out code blocks
620
+ - [ ] Created list of code safe to remove
621
+ - [ ] Verified removal won't break dynamic imports or reflection
622
+ - [ ] Proposed ideas for any dead code worth removing
623
+ `;
624
+ }
625
+ function factsVerification() {
626
+ return dedent`
627
+ # Facts Verification
628
+
629
+ 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.
630
+
631
+ ## Scope
632
+
633
+ Focus on these areas:
634
+
635
+ 1. **Accuracy** - Do documented facts reflect the current codebase?
636
+ 2. **Completeness** - Are important implementation details documented?
637
+ 3. **Staleness** - Have facts become outdated due to recent changes?
638
+ 4. **Relevance** - Are all facts still useful for understanding the project?
639
+
640
+ ## Blocked By
641
+
642
+ (none)
643
+
644
+ ## Definition of Done
645
+
646
+ - [ ] Read each fact file in \`.dust/facts/\`
647
+ - [ ] Verified each fact against current codebase
648
+ - [ ] Identified outdated or inaccurate facts
649
+ - [ ] Listed missing facts that would help agents
650
+ - [ ] Updated or removed stale facts
651
+ - [ ] Proposed ideas for any facts improvements needed
652
+ `;
653
+ }
654
+ function ideasFromCommits() {
655
+ return dedent`
656
+ # Ideas from Commits
657
+
658
+ 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.
659
+
660
+ ## Scope
661
+
662
+ Focus on these areas:
663
+
664
+ 1. **Technical debt** - Did recent work introduce shortcuts?
665
+ 2. **Incomplete work** - Are there TODO comments or partial implementations?
666
+ 3. **Pattern opportunities** - Can recent changes be generalized?
667
+ 4. **Test gaps** - Do recent changes have adequate test coverage?
668
+
669
+ ## Blocked By
670
+
671
+ (none)
672
+
673
+ ## Definition of Done
674
+
675
+ - [ ] Reviewed commits from the last 20 commits
676
+ - [ ] Identified patterns or shortcuts worth addressing
677
+ - [ ] Listed TODO comments added in recent commits
678
+ - [ ] Noted areas where changes could be generalized
679
+ - [ ] Proposed follow-up ideas for any issues identified
680
+ `;
681
+ }
682
+ function ideasFromGoals() {
683
+ return dedent`
684
+ # Ideas from Goals
685
+
686
+ Review \`.dust/goals/\` 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.
687
+
688
+ ## Scope
689
+
690
+ Focus on these areas:
691
+
692
+ 1. **Unmet goals** - Which goals lack supporting work?
693
+ 2. **Gap analysis** - Where does the codebase fall short of goals?
694
+ 3. **New opportunities** - What work would better achieve each goal?
695
+ 4. **Goal alignment** - Are current tasks aligned with stated goals?
696
+
697
+ ## Blocked By
698
+
699
+ (none)
700
+
701
+ ## Definition of Done
702
+
703
+ - [ ] Read each goal file in \`.dust/goals/\`
704
+ - [ ] Analyzed codebase for alignment with each goal
705
+ - [ ] Listed gaps between current state and goal intent
706
+ - [ ] Proposed new ideas for unmet or underserved goals
707
+ `;
708
+ }
709
+ function performanceReview() {
710
+ return dedent`
711
+ # Performance Review
712
+
713
+ 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.
714
+
715
+ ## Scope
716
+
717
+ Focus on these areas:
718
+
719
+ 1. **Startup time** - How fast does the application start?
720
+ 2. **Command latency** - How responsive are CLI commands?
721
+ 3. **Memory usage** - Is memory being used efficiently?
722
+ 4. **Build performance** - How fast is the build process?
723
+ 5. **Test speed** - Are tests running efficiently?
724
+
725
+ ## Blocked By
726
+
727
+ (none)
728
+
729
+ ## Definition of Done
730
+
731
+ - [ ] Measured startup time for common commands
732
+ - [ ] Profiled memory usage during typical operations
733
+ - [ ] Identified slow commands or operations
734
+ - [ ] Listed optimization opportunities by impact
735
+ - [ ] Proposed ideas for any performance improvements identified
736
+ `;
737
+ }
738
+ function securityReview() {
739
+ return dedent`
740
+ # Security Review
741
+
742
+ 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.
743
+
744
+ ## Scope
745
+
746
+ Focus on these areas:
747
+
748
+ 1. **Hardcoded secrets** - API keys, passwords, tokens in source code
749
+ 2. **Injection vulnerabilities** - SQL injection, command injection, XSS
750
+ 3. **Authentication issues** - Weak password handling, missing auth checks
751
+ 4. **Sensitive data exposure** - Logging sensitive data, insecure storage
752
+ 5. **Dependency vulnerabilities** - Known CVEs in dependencies
753
+
754
+ ## Blocked By
755
+
756
+ (none)
757
+
758
+ ## Definition of Done
759
+
760
+ - [ ] Searched for hardcoded secrets (API keys, passwords, tokens)
761
+ - [ ] Reviewed input validation and sanitization
762
+ - [ ] Checked authentication and authorization logic
763
+ - [ ] Verified sensitive data is not logged or exposed
764
+ - [ ] Ran dependency audit for known vulnerabilities
765
+ - [ ] Documented any findings with severity ratings
766
+ - [ ] Proposed ideas for any security issues found
767
+ `;
768
+ }
769
+ function staleIdeas() {
770
+ return dedent`
771
+ # Stale Ideas
772
+
773
+ 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.
774
+
775
+ ## Scope
776
+
777
+ Focus on these areas:
778
+
779
+ 1. **Age** - Ideas unchanged for many commits may need attention
780
+ 2. **Relevance** - Has the project evolved past the idea?
781
+ 3. **Actionability** - Can the idea be converted to a task?
782
+ 4. **Duplication** - Are there overlapping or redundant ideas?
783
+
784
+ ## Blocked By
785
+
786
+ (none)
787
+
788
+ ## Definition of Done
789
+
790
+ - [ ] Listed all ideas with their last modification date
791
+ - [ ] Identified ideas unchanged for 50+ commits
792
+ - [ ] Reviewed each stale idea for current relevance
793
+ - [ ] Promoted actionable ideas to tasks
794
+ - [ ] Deleted ideas that are no longer relevant
795
+ `;
796
+ }
797
+ function testCoverage() {
798
+ return dedent`
799
+ # Test Coverage
800
+
801
+ 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.
802
+
803
+ ## Scope
804
+
805
+ Focus on these areas:
806
+
807
+ 1. **Core business logic** - Functions that handle critical operations
808
+ 2. **Edge cases** - Boundary conditions, error handling paths
809
+ 3. **Integration points** - API endpoints, database operations
810
+ 4. **User-facing features** - UI components, form validation
811
+ 5. **Recent changes** - Code modified in the last few commits
812
+
813
+ ## Blocked By
814
+
815
+ (none)
816
+
817
+ ## Definition of Done
818
+
819
+ - [ ] Identified modules with low or no test coverage
820
+ - [ ] Listed critical paths that lack tests
821
+ - [ ] Prioritized areas by risk and importance
822
+ - [ ] Proposed ideas for any test coverage gaps identified
823
+ `;
824
+ }
825
+ var stockAuditFunctions = {
826
+ "agent-developer-experience": agentDeveloperExperience,
827
+ "dead-code": deadCode,
828
+ "facts-verification": factsVerification,
829
+ "ideas-from-commits": ideasFromCommits,
830
+ "ideas-from-goals": ideasFromGoals,
831
+ "performance-review": performanceReview,
832
+ "security-review": securityReview,
833
+ "stale-ideas": staleIdeas,
834
+ "test-coverage": testCoverage
835
+ };
836
+ function loadStockAudits() {
837
+ return Object.entries(stockAuditFunctions).sort(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
838
+ const template = render();
839
+ const description = extractOpeningSentence(template);
840
+ return { name, description, template };
841
+ });
842
+ }
843
+
408
844
  // lib/cli/colors.ts
409
845
  var ANSI_COLORS = {
410
846
  reset: "\x1B[0m",
@@ -439,17 +875,6 @@ function getColors() {
439
875
  }
440
876
 
441
877
  // lib/cli/commands/audit.ts
442
- var __dirname3 = dirname2(fileURLToPath2(import.meta.url));
443
- var stockAuditsDir = join5(__dirname3, "../../templates/audits");
444
- function loadStockAudits() {
445
- const files = readdirSync(stockAuditsDir).filter((f) => f.endsWith(".md")).sort();
446
- return files.map((file) => {
447
- const template = readFileSync2(join5(stockAuditsDir, file), "utf-8");
448
- const name = basename(file, ".md");
449
- const description = extractOpeningSentence(template);
450
- return { name, description, template };
451
- });
452
- }
453
878
  function transformAuditContent(content) {
454
879
  const titleMatch = content.match(/^#\s+(.+)$/m);
455
880
  if (!titleMatch) {
@@ -544,13 +969,13 @@ async function audit(dependencies) {
544
969
 
545
970
  // lib/cli/commands/bucket.ts
546
971
  import { spawn as nodeSpawn3 } from "node:child_process";
547
- import { accessSync } from "node:fs";
972
+ import { accessSync, statSync } from "node:fs";
548
973
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
549
974
  import { createServer as httpCreateServer } from "node:http";
550
975
  import { homedir, tmpdir } from "node:os";
551
976
 
552
977
  // lib/bucket/auth.ts
553
- import { join as join6 } from "node:path";
978
+ import { join as join4 } from "node:path";
554
979
  var CREDENTIALS_DIR = ".dust";
555
980
  var CREDENTIALS_FILE = "credentials.json";
556
981
  var AUTH_TIMEOUT_MS = 120000;
@@ -559,7 +984,7 @@ function getDustbucketHost() {
559
984
  return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
560
985
  }
561
986
  function credentialsPath(homeDir) {
562
- return join6(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
987
+ return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
563
988
  }
564
989
  async function loadStoredToken(fileSystem, homeDir) {
565
990
  const path = credentialsPath(homeDir);
@@ -572,7 +997,7 @@ async function loadStoredToken(fileSystem, homeDir) {
572
997
  }
573
998
  }
574
999
  async function storeToken(fileSystem, homeDir, token) {
575
- const dirPath = join6(homeDir, CREDENTIALS_DIR);
1000
+ const dirPath = join4(homeDir, CREDENTIALS_DIR);
576
1001
  await fileSystem.mkdir(dirPath, { recursive: true });
577
1002
  await fileSystem.writeFile(credentialsPath(homeDir), JSON.stringify({ token }));
578
1003
  }
@@ -694,7 +1119,7 @@ function getLogLines(buffer) {
694
1119
  }
695
1120
 
696
1121
  // lib/bucket/repository.ts
697
- import { join as join7 } from "node:path";
1122
+ import { join as join6 } from "node:path";
698
1123
 
699
1124
  // lib/agent-events.ts
700
1125
  function rawEventToAgentEvent(rawEvent) {
@@ -1116,12 +1541,28 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1116
1541
 
1117
1542
  // lib/cli/commands/loop.ts
1118
1543
  import { spawn as nodeSpawn2 } from "node:child_process";
1544
+ import { readFileSync } from "node:fs";
1545
+ import os from "node:os";
1546
+ import { dirname, join as join5 } from "node:path";
1547
+ import { fileURLToPath } from "node:url";
1548
+
1549
+ // lib/workflow-tasks.ts
1550
+ var IDEA_TRANSITION_PREFIXES = [
1551
+ "Refine Idea: ",
1552
+ "Decompose Idea: ",
1553
+ "Shelve Idea: "
1554
+ ];
1555
+ var BUILD_IDEA_PREFIX = "Build Idea: ";
1556
+ function titleToFilename(title) {
1557
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
1558
+ }
1119
1559
 
1120
1560
  // lib/cli/commands/focus.ts
1121
- function buildImplementationInstructions(bin, hooksInstalled) {
1561
+ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
1122
1562
  const steps = [];
1123
1563
  let step = 1;
1124
- steps.push(`Note: Skip the \`${bin} agent\` step - your task has already been specified below.`, "");
1564
+ const hasIdeaFile = !taskTitle?.startsWith(BUILD_IDEA_PREFIX);
1565
+ steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
1125
1566
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
1126
1567
  step++;
1127
1568
  steps.push(`${step}. Implement the task`);
@@ -1130,7 +1571,16 @@ function buildImplementationInstructions(bin, hooksInstalled) {
1130
1571
  steps.push(`${step}. Run \`${bin} check\` before committing`);
1131
1572
  step++;
1132
1573
  }
1133
- steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
1574
+ const commitMessageLine = taskTitle ? ` Use this exact commit message: "${taskTitle}". Do not add any prefix.` : ' Use the task title as the commit message. Do not add prefixes like "Complete task:" - use the title directly.';
1575
+ const commitItems = [
1576
+ " - All implementation changes",
1577
+ " - Deletion of the completed task file",
1578
+ " - Updates to any facts that changed"
1579
+ ];
1580
+ if (hasIdeaFile) {
1581
+ commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
1582
+ }
1583
+ steps.push(`${step}. Create a single atomic commit that includes:`, ...commitItems, "", commitMessageLine, "");
1134
1584
  step++;
1135
1585
  steps.push(`${step}. Push your commit to the remote repository`);
1136
1586
  steps.push("");
@@ -1234,6 +1684,29 @@ async function next(dependencies) {
1234
1684
  }
1235
1685
 
1236
1686
  // lib/cli/commands/loop.ts
1687
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
1688
+ function getDustVersion() {
1689
+ const candidates = [
1690
+ join5(__dirname2, "../../../package.json"),
1691
+ join5(__dirname2, "../package.json")
1692
+ ];
1693
+ for (const candidate of candidates) {
1694
+ try {
1695
+ const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
1696
+ return packageJson.version ?? "unknown";
1697
+ } catch {}
1698
+ }
1699
+ return "unknown";
1700
+ }
1701
+ function getEnvironmentContext(cwd) {
1702
+ return {
1703
+ machineName: os.hostname(),
1704
+ cwd,
1705
+ platform: `${os.platform()} ${os.release()}`,
1706
+ dustVersion: getDustVersion(),
1707
+ runtimeVersion: process.version
1708
+ };
1709
+ }
1237
1710
  function formatLoopEvent(event) {
1238
1711
  switch (event.type) {
1239
1712
  case "loop.warning":
@@ -1333,7 +1806,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
1333
1806
  type: "loop.sync_skipped",
1334
1807
  reason: pullResult.message
1335
1808
  });
1336
- const prompt2 = `Note: Skip the \`dust agent\` step - your task has already been specified below.
1809
+ const prompt2 = `Note: Do NOT run \`dust agent\`.
1337
1810
 
1338
1811
  git pull failed with the following error:
1339
1812
 
@@ -1350,7 +1823,8 @@ Make sure the repository is in a clean state and synced with remote before finis
1350
1823
  title: "Resolving git conflict",
1351
1824
  prompt: prompt2,
1352
1825
  agentType: "claude",
1353
- purpose: "git-conflict"
1826
+ purpose: "git-conflict",
1827
+ ...getEnvironmentContext(context.cwd)
1354
1828
  });
1355
1829
  try {
1356
1830
  await run2(prompt2, {
@@ -1383,14 +1857,14 @@ Make sure the repository is in a clean state and synced with remote before finis
1383
1857
  onLoopEvent({ type: "loop.tasks_found" });
1384
1858
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
1385
1859
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
1386
- const instructions = buildImplementationInstructions(dustCommand, true);
1860
+ const instructions = buildImplementationInstructions(dustCommand, true, task.title ?? undefined);
1387
1861
  const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
1388
1862
 
1389
- ## Task: ${task.title}
1390
-
1391
1863
  The following is the contents of the task file \`${task.path}\`:
1392
1864
 
1865
+ ----------
1393
1866
  ${taskContent}
1867
+ ----------
1394
1868
 
1395
1869
  When the task is complete, delete the task file \`${task.path}\`.
1396
1870
 
@@ -1402,7 +1876,8 @@ ${instructions}`;
1402
1876
  title: task.title ?? task.path,
1403
1877
  prompt,
1404
1878
  agentType: "claude",
1405
- purpose: "task"
1879
+ purpose: "task",
1880
+ ...getEnvironmentContext(context.cwd)
1406
1881
  });
1407
1882
  try {
1408
1883
  await run2(prompt, {
@@ -1512,7 +1987,7 @@ function parseRepository(data) {
1512
1987
  }
1513
1988
  function getRepoTempPath(repoName, tempDir) {
1514
1989
  const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
1515
- return join7(tempDir, `dust-bucket-${safeName}`);
1990
+ return join6(tempDir, `dust-bucket-${safeName}`);
1516
1991
  }
1517
1992
  async function cloneRepository(repository, targetPath, spawn, context) {
1518
1993
  return new Promise((resolve) => {
@@ -1966,7 +2441,7 @@ function renderTabs(state) {
1966
2441
  const width = name.length + 4;
1967
2442
  if (i === state.selectedIndex) {
1968
2443
  tabs.push({
1969
- text: `${ANSI.INVERSE} ${dot}${ANSI.INVERSE}${color} ${name} ${ANSI.RESET}`,
2444
+ text: ` ${dot}${color} ${ANSI.INVERSE}${name}${ANSI.RESET} `,
1970
2445
  width
1971
2446
  });
1972
2447
  } else {
@@ -2230,6 +2705,13 @@ function createDefaultBucketDependencies() {
2230
2705
  return false;
2231
2706
  }
2232
2707
  },
2708
+ isDirectory: (path) => {
2709
+ try {
2710
+ return statSync(path).isDirectory();
2711
+ } catch {
2712
+ return false;
2713
+ }
2714
+ },
2233
2715
  readFile: (path) => readFile(path, "utf8"),
2234
2716
  writeFile: (path, content) => writeFile(path, content, "utf8"),
2235
2717
  mkdir: (path, options) => mkdir(path, options).then(() => {}),
@@ -2478,9 +2960,10 @@ function createKeypressHandler(useTUI, state, onQuit, options) {
2478
2960
  onQuit();
2479
2961
  };
2480
2962
  }
2481
- async function resolveToken(commandArgs, authDeps, context) {
2482
- if (commandArgs[0]) {
2483
- return commandArgs[0];
2963
+ async function resolveToken(authDeps, context) {
2964
+ const envToken = process.env.DUST_BUCKET_TOKEN;
2965
+ if (envToken) {
2966
+ return envToken;
2484
2967
  }
2485
2968
  const stored = await loadStoredToken(authDeps.fileSystem, authDeps.getHomeDir());
2486
2969
  if (stored) {
@@ -2498,8 +2981,8 @@ async function resolveToken(commandArgs, authDeps, context) {
2498
2981
  }
2499
2982
  }
2500
2983
  async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
2501
- const { arguments: commandArgs, context, fileSystem } = dependencies;
2502
- const token = await resolveToken(commandArgs, bucketDeps.auth, context);
2984
+ const { context, fileSystem } = dependencies;
2985
+ const token = await resolveToken(bucketDeps.auth, context);
2503
2986
  if (!token) {
2504
2987
  return { exitCode: 1 };
2505
2988
  }
@@ -2614,22 +3097,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
2614
3097
  }
2615
3098
 
2616
3099
  // lib/cli/commands/lint-markdown.ts
2617
- import { dirname as dirname3, resolve } from "node:path";
2618
-
2619
- // lib/workflow-tasks.ts
2620
- var IDEA_TRANSITION_PREFIXES = [
2621
- "Refine Idea: ",
2622
- "Decompose Idea: ",
2623
- "Shelve Idea: "
2624
- ];
2625
- function titleToFilename(title) {
2626
- return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
2627
- }
2628
-
2629
- // lib/cli/commands/lint-markdown.ts
3100
+ import { dirname as dirname2, join as join7, resolve } from "node:path";
2630
3101
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
2631
3102
  var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
2632
3103
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
3104
+ var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
2633
3105
  var MAX_OPENING_SENTENCE_LENGTH = 150;
2634
3106
  function validateFilename(filePath) {
2635
3107
  const parts = filePath.split("/");
@@ -2727,7 +3199,7 @@ function validateLinks(filePath, content, fileSystem) {
2727
3199
  const violations = [];
2728
3200
  const lines = content.split(`
2729
3201
  `);
2730
- const fileDir = dirname3(filePath);
3202
+ const fileDir = dirname2(filePath);
2731
3203
  for (let i = 0;i < lines.length; i++) {
2732
3204
  const line = lines[i];
2733
3205
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -2844,7 +3316,7 @@ function validateSemanticLinks(filePath, content) {
2844
3316
  const violations = [];
2845
3317
  const lines = content.split(`
2846
3318
  `);
2847
- const fileDir = dirname3(filePath);
3319
+ const fileDir = dirname2(filePath);
2848
3320
  let currentSection = null;
2849
3321
  for (let i = 0;i < lines.length; i++) {
2850
3322
  const line = lines[i];
@@ -2927,7 +3399,7 @@ function validateGoalHierarchyLinks(filePath, content) {
2927
3399
  const violations = [];
2928
3400
  const lines = content.split(`
2929
3401
  `);
2930
- const fileDir = dirname3(filePath);
3402
+ const fileDir = dirname2(filePath);
2931
3403
  let currentSection = null;
2932
3404
  for (let i = 0;i < lines.length; i++) {
2933
3405
  const line = lines[i];
@@ -2977,7 +3449,7 @@ function validateGoalHierarchyLinks(filePath, content) {
2977
3449
  function extractGoalRelationships(filePath, content) {
2978
3450
  const lines = content.split(`
2979
3451
  `);
2980
- const fileDir = dirname3(filePath);
3452
+ const fileDir = dirname2(filePath);
2981
3453
  const parentGoals = [];
2982
3454
  const subGoals = [];
2983
3455
  let currentSection = null;
@@ -3083,6 +3555,72 @@ async function safeScanDir(glob, dirPath) {
3083
3555
  throw error;
3084
3556
  }
3085
3557
  }
3558
+ async function validateContentDirectoryFiles(dirPath, fileSystem) {
3559
+ const violations = [];
3560
+ let entries;
3561
+ try {
3562
+ entries = await fileSystem.readdir(dirPath);
3563
+ } catch (error) {
3564
+ if (error.code === "ENOENT") {
3565
+ return [];
3566
+ }
3567
+ throw error;
3568
+ }
3569
+ for (const entry of entries) {
3570
+ const entryPath = `${dirPath}/${entry}`;
3571
+ if (entry.startsWith(".")) {
3572
+ violations.push({
3573
+ file: entryPath,
3574
+ message: `Hidden file "${entry}" found in content directory`
3575
+ });
3576
+ continue;
3577
+ }
3578
+ if (fileSystem.isDirectory(entryPath)) {
3579
+ violations.push({
3580
+ file: entryPath,
3581
+ message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
3582
+ });
3583
+ continue;
3584
+ }
3585
+ if (!entry.endsWith(".md")) {
3586
+ violations.push({
3587
+ file: entryPath,
3588
+ message: `Non-markdown file "${entry}" found in content directory`
3589
+ });
3590
+ }
3591
+ }
3592
+ return violations;
3593
+ }
3594
+ async function validateDirectoryStructure(dustPath, fileSystem, extraDirectories = []) {
3595
+ const violations = [];
3596
+ let entries;
3597
+ try {
3598
+ entries = await fileSystem.readdir(dustPath);
3599
+ } catch (error) {
3600
+ if (error.code === "ENOENT") {
3601
+ return [];
3602
+ }
3603
+ throw error;
3604
+ }
3605
+ const allowedDirectories = new Set([
3606
+ ...EXPECTED_DIRECTORIES,
3607
+ ...extraDirectories
3608
+ ]);
3609
+ for (const entry of entries) {
3610
+ const entryPath = `${dustPath}/${entry}`;
3611
+ if (!fileSystem.isDirectory(entryPath)) {
3612
+ continue;
3613
+ }
3614
+ if (!allowedDirectories.has(entry)) {
3615
+ const allowedList = [...allowedDirectories].sort().join(", ");
3616
+ violations.push({
3617
+ file: entryPath,
3618
+ message: `Unexpected directory "${entry}" in .dust/. Allowed directories: ${allowedList}. To allow this directory, add it to "extraDirectories" in .dust/config/settings.json`
3619
+ });
3620
+ }
3621
+ }
3622
+ return violations;
3623
+ }
3086
3624
  async function lintMarkdown(dependencies) {
3087
3625
  const { context, fileSystem, globScanner: glob } = dependencies;
3088
3626
  const dustPath = `${context.cwd}/.dust`;
@@ -3094,6 +3632,26 @@ async function lintMarkdown(dependencies) {
3094
3632
  }
3095
3633
  const dustFiles = dustScan.files;
3096
3634
  const violations = [];
3635
+ context.stdout("Validating directory structure...");
3636
+ violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
3637
+ const settingsPath = join7(dustPath, "config", "settings.json");
3638
+ if (fileSystem.exists(settingsPath)) {
3639
+ context.stdout("Validating settings.json...");
3640
+ try {
3641
+ const settingsContent = await fileSystem.readFile(settingsPath);
3642
+ const settingsViolations = validateSettingsJson(settingsContent);
3643
+ for (const sv of settingsViolations) {
3644
+ violations.push({
3645
+ file: settingsPath,
3646
+ message: sv.message
3647
+ });
3648
+ }
3649
+ } catch (error) {
3650
+ if (error.code !== "ENOENT") {
3651
+ throw error;
3652
+ }
3653
+ }
3654
+ }
3097
3655
  context.stdout("Validating links in .dust/...");
3098
3656
  for (const file of dustFiles) {
3099
3657
  if (!file.endsWith(".md"))
@@ -3110,6 +3668,10 @@ async function lintMarkdown(dependencies) {
3110
3668
  }
3111
3669
  const contentDirs = ["goals", "facts", "ideas", "tasks"];
3112
3670
  context.stdout("Validating content files...");
3671
+ for (const dir of contentDirs) {
3672
+ const dirPath = `${dustPath}/${dir}`;
3673
+ violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
3674
+ }
3113
3675
  for (const dir of contentDirs) {
3114
3676
  const dirPath = `${dustPath}/${dir}`;
3115
3677
  const { files } = await safeScanDir(glob, dirPath);
@@ -3276,8 +3838,8 @@ async function runValidationCheck(dependencies) {
3276
3838
  });
3277
3839
  const durationMs = Date.now() - startTime;
3278
3840
  return {
3279
- name: "lint markdown",
3280
- command: "dust lint markdown",
3841
+ name: "lint",
3842
+ command: "dust lint",
3281
3843
  exitCode: result.exitCode,
3282
3844
  output: outputLines.join(`
3283
3845
  `),
@@ -3374,7 +3936,45 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
3374
3936
 
3375
3937
  // lib/cli/commands/help.ts
3376
3938
  function generateHelpText(settings) {
3377
- return loadTemplate("help", { bin: settings.dustCommand });
3939
+ const bin = settings.dustCommand;
3940
+ return dedent`
3941
+ 💨 dust - Flow state for AI coding agents.
3942
+
3943
+ Usage: ${bin} <command> [options]
3944
+
3945
+ Commands:
3946
+ init Initialize a new Dust repository
3947
+ lint Run lint checks on .dust/ files
3948
+ list List all items (tasks, ideas, goals, facts)
3949
+ tasks List tasks (actionable work with definitions of done)
3950
+ ideas List ideas (vague proposals, convert to tasks when ready)
3951
+ goals List goals (guiding principles, stable, rarely change)
3952
+ facts List facts (documentation of current system state)
3953
+ next Show tasks ready to work on (not blocked)
3954
+ check Run project-defined quality gate hook
3955
+ agent Agent greeting and routing instructions
3956
+ focus Declare current objective (for remote session tracking)
3957
+ pick task Pick the next task to work on
3958
+ implement task Implement a task
3959
+ new task Create a new task
3960
+ new goal Create a new goal
3961
+ new idea Create a new idea
3962
+ loop claude Run continuous Claude iteration on tasks
3963
+ pre push Git pre-push hook validation
3964
+ help Show this help message
3965
+
3966
+ 🤖 Agent Guide
3967
+
3968
+ Dust is a lightweight planning system. The .dust/ directory contains:
3969
+ - goals/ - Guiding principles (stable, rarely change)
3970
+ - ideas/ - Proposals (convert to tasks when ready)
3971
+ - tasks/ - Actionable work with definitions of done
3972
+ - facts/ - Documentation of current system state
3973
+
3974
+ Workflow: Pick a task → implement it → delete the task file → commit atomically.
3975
+
3976
+ Run \`${bin} agent\` to get started!
3977
+ `;
3378
3978
  }
3379
3979
  async function help(dependencies) {
3380
3980
  dependencies.context.stdout(generateHelpText(dependencies.settings));
@@ -3405,6 +4005,24 @@ var USE_DUST_FACT = `# Use dust for planning
3405
4005
 
3406
4006
  This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
3407
4007
  `;
4008
+ function claudeMdContent(dustCommand) {
4009
+ return dedent`
4010
+ # Claude Code Instructions
4011
+
4012
+ This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4013
+
4014
+ Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4015
+ `;
4016
+ }
4017
+ function agentsMdContent(dustCommand) {
4018
+ return dedent`
4019
+ # Agent Instructions
4020
+
4021
+ This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4022
+
4023
+ Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4024
+ `;
4025
+ }
3408
4026
  async function init(dependencies) {
3409
4027
  const { context, fileSystem } = dependencies;
3410
4028
  const colors = getColors();
@@ -3443,8 +4061,9 @@ async function init(dependencies) {
3443
4061
  }
3444
4062
  const claudeMdPath = `${context.cwd}/CLAUDE.md`;
3445
4063
  try {
3446
- const claudeContent = loadTemplate("claude-md", { dustCommand });
3447
- await fileSystem.writeFile(claudeMdPath, claudeContent, { flag: "wx" });
4064
+ await fileSystem.writeFile(claudeMdPath, claudeMdContent(dustCommand), {
4065
+ flag: "wx"
4066
+ });
3448
4067
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
3449
4068
  } catch (error) {
3450
4069
  if (error.code === "EEXIST") {
@@ -3455,8 +4074,9 @@ async function init(dependencies) {
3455
4074
  }
3456
4075
  const agentsMdPath = `${context.cwd}/AGENTS.md`;
3457
4076
  try {
3458
- const agentsContent = loadTemplate("agents-md", { dustCommand });
3459
- await fileSystem.writeFile(agentsMdPath, agentsContent, { flag: "wx" });
4077
+ await fileSystem.writeFile(agentsMdPath, agentsMdContent(dustCommand), {
4078
+ flag: "wx"
4079
+ });
3460
4080
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
3461
4081
  } catch (error) {
3462
4082
  if (error.code === "EEXIST") {
@@ -3603,23 +4223,147 @@ async function list(dependencies) {
3603
4223
  return { exitCode: 0 };
3604
4224
  }
3605
4225
 
3606
- // lib/cli/template-command.ts
3607
- var createTemplateCommand = (templateName) => async (dependencies) => {
4226
+ // lib/cli/commands/new-goal.ts
4227
+ function newGoalInstructions(vars) {
4228
+ const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
4229
+ return dedent`
4230
+ ## Adding a New Goal
4231
+
4232
+ Goals are guiding principles that persist across tasks. They define the "why" behind the work.
4233
+
4234
+ ${intro}
4235
+ 1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
4236
+ 2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
4237
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
4238
+ 4. Write a clear description explaining:
4239
+ - What this goal means in practice
4240
+ - Why it matters for the project
4241
+ - How to evaluate whether work supports this goal
4242
+ 5. Run \`${vars.bin} lint\` to catch any formatting issues
4243
+ 6. Create a single atomic commit with a message in the format "Add goal: <title>"
4244
+ 7. Push your commit to the remote repository
4245
+
4246
+ Goals should be:
4247
+ - **Stable** - They rarely change once established
4248
+ - **Actionable** - Tasks can be linked to them
4249
+ - **Clear** - Anyone reading should understand what it means
4250
+ `;
4251
+ }
4252
+ async function newGoal(dependencies) {
3608
4253
  const { context, settings } = dependencies;
3609
4254
  const hooksInstalled = await manageGitHooks(dependencies);
3610
4255
  const vars = templateVariables(settings, hooksInstalled);
3611
- context.stdout(loadTemplate(templateName, vars));
4256
+ context.stdout(newGoalInstructions(vars));
3612
4257
  return { exitCode: 0 };
3613
- };
3614
-
3615
- // lib/cli/commands/new-goal.ts
3616
- var newGoal = createTemplateCommand("agent-new-goal");
4258
+ }
3617
4259
 
3618
4260
  // lib/cli/commands/new-idea.ts
3619
- var newIdea = createTemplateCommand("agent-new-idea");
4261
+ function newIdeaInstructions(vars) {
4262
+ return dedent`
4263
+ ## Adding a New Idea
4264
+
4265
+ Follow these steps:
4266
+
4267
+ 1. Run \`${vars.bin} ideas\` to see all existing ideas and avoid duplicates
4268
+ 2. Create a new markdown file in \`.dust/ideas/\` with a descriptive kebab-case name (e.g., \`improve-error-messages.md\`)
4269
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Improve error messages\`)
4270
+ 4. Write a brief description of the potential change or improvement
4271
+ 5. If the idea has open questions, add an \`## Open Questions\` section (see below)
4272
+ 6. Run \`${vars.bin} lint\` to catch any issues with the idea file format
4273
+ 7. Create a single atomic commit with a message in the format "Add idea: <title>"
4274
+ 8. Push your commit to the remote repository
4275
+
4276
+ ### Open Questions section
4277
+
4278
+ Ideas exist to eventually spawn tasks, so they start intentionally vague. An optional \`## Open Questions\` section captures the decisions that need to be made before the idea becomes actionable. Each question is an h3 heading ending with \`?\`, and each option is an h4 heading with markdown content explaining the trade-offs:
4279
+
4280
+ \`\`\`markdown
4281
+ ## Open Questions
4282
+
4283
+ ### Should we take our own payments?
4284
+
4285
+ #### Yes, take our own payments
4286
+
4287
+ Lower costs and we become the seller of record, but requires a merchant account.
4288
+
4289
+ #### No, use a payment provider
4290
+
4291
+ Higher costs but simpler setup. No merchant account needed.
4292
+
4293
+ ### Which storage backend should we use?
4294
+
4295
+ #### SQLite
4296
+
4297
+ Simple and embedded. Good for single-node deployments.
4298
+
4299
+ #### PostgreSQL
4300
+
4301
+ Scalable but requires a separate server.
4302
+ \`\`\`
4303
+
4304
+ Rules:
4305
+ - Questions are \`###\` headings and must end with \`?\`
4306
+ - Options are \`####\` headings beneath a question
4307
+ - Each question must have at least one option
4308
+ - Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
4309
+ `;
4310
+ }
4311
+ async function newIdea(dependencies) {
4312
+ const { context, settings } = dependencies;
4313
+ const hooksInstalled = await manageGitHooks(dependencies);
4314
+ const vars = templateVariables(settings, hooksInstalled);
4315
+ context.stdout(newIdeaInstructions(vars));
4316
+ return { exitCode: 0 };
4317
+ }
3620
4318
 
3621
4319
  // lib/cli/commands/new-task.ts
3622
- var newTask = createTemplateCommand("agent-new-task");
4320
+ function newTaskInstructions(vars) {
4321
+ const steps = [];
4322
+ steps.push("## Adding a New Task");
4323
+ steps.push("");
4324
+ if (vars.isClaudeCodeWeb) {
4325
+ steps.push("Follow these steps to create the task definition, then spawn a sub-agent for implementation.");
4326
+ steps.push("");
4327
+ steps.push("Use a todo list to track your progress through these steps.");
4328
+ } else {
4329
+ steps.push("Follow these steps:");
4330
+ }
4331
+ steps.push("");
4332
+ steps.push(`1. Run \`${vars.bin} ideas\` to see all existing ideas`);
4333
+ steps.push("2. Determine which ideas (if any) should be:");
4334
+ steps.push(" - **Deleted** - if the new task fully covers the idea");
4335
+ steps.push(" - **Updated** - if the idea's scope changes as a result of the task");
4336
+ steps.push("3. Research thoroughly to ensure the task will be clearly defined:");
4337
+ steps.push(" - Explore the codebase to understand existing patterns and relevant files");
4338
+ steps.push(" - Identify exactly which files need to change and how");
4339
+ steps.push(" - Resolve any ambiguities in the requirements before writing the task");
4340
+ steps.push(" - Gather specific technical details (function names, file paths, data structures)");
4341
+ steps.push(" - The goal is a task description with minimal ambiguity at implementation time");
4342
+ steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
4343
+ steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
4344
+ steps.push('6. Write a comprehensive description starting with an imperative opening sentence (e.g., "Add caching to the API layer." not "This task adds caching."). Include technical details and references to relevant files.');
4345
+ steps.push("7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)");
4346
+ steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
4347
+ steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
4348
+ steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
4349
+ steps.push('11. Create a single atomic commit with a message in the format "Add task: <title>" that includes:');
4350
+ steps.push(" - The new task file");
4351
+ steps.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
4352
+ if (vars.isClaudeCodeWeb) {
4353
+ steps.push(`12. **Start a sub-agent** to implement the task: "Run \`${vars.bin} implement task\` and implement the task in \`.dust/tasks/[task-file].md\`"`);
4354
+ } else {
4355
+ steps.push("12. Push your commit to the remote repository");
4356
+ }
4357
+ return steps.join(`
4358
+ `);
4359
+ }
4360
+ async function newTask(dependencies) {
4361
+ const { context, settings } = dependencies;
4362
+ const hooksInstalled = await manageGitHooks(dependencies);
4363
+ const vars = templateVariables(settings, hooksInstalled);
4364
+ context.stdout(newTaskInstructions(vars));
4365
+ return { exitCode: 0 };
4366
+ }
3623
4367
 
3624
4368
  // lib/cli/commands/pick-task.ts
3625
4369
  async function pickTask(dependencies) {
@@ -3791,7 +4535,7 @@ async function facts(dependencies) {
3791
4535
  // lib/cli/main.ts
3792
4536
  var commandRegistry = {
3793
4537
  init,
3794
- "lint markdown": lintMarkdown,
4538
+ lint: lintMarkdown,
3795
4539
  list,
3796
4540
  tasks,
3797
4541
  goals,
@@ -3860,6 +4604,13 @@ async function main(options) {
3860
4604
  function createFileSystem(primitives) {
3861
4605
  return {
3862
4606
  exists: primitives.existsSync,
4607
+ isDirectory: (path) => {
4608
+ try {
4609
+ return primitives.statSync(path).isDirectory();
4610
+ } catch {
4611
+ return false;
4612
+ }
4613
+ },
3863
4614
  readFile: (path) => primitives.readFile(path, "utf-8"),
3864
4615
  writeFile: (path, content, options) => primitives.writeFile(path, content, {
3865
4616
  encoding: "utf-8",
@@ -3899,4 +4650,4 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
3899
4650
  }
3900
4651
 
3901
4652
  // lib/cli/run.ts
3902
- await wireEntry({ existsSync, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2 }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });
4653
+ await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2 }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });