@joshski/dust 0.1.42 → 0.1.44

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,14 @@ 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
- import { homedir, tmpdir } from "node:os";
975
+ import { homedir } from "node:os";
976
+ import { join as join7 } from "node:path";
551
977
 
552
978
  // lib/bucket/auth.ts
553
- import { join as join6 } from "node:path";
979
+ import { join as join4 } from "node:path";
554
980
  var CREDENTIALS_DIR = ".dust";
555
981
  var CREDENTIALS_FILE = "credentials.json";
556
982
  var AUTH_TIMEOUT_MS = 120000;
@@ -559,7 +985,7 @@ function getDustbucketHost() {
559
985
  return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
560
986
  }
561
987
  function credentialsPath(homeDir) {
562
- return join6(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
988
+ return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
563
989
  }
564
990
  async function loadStoredToken(fileSystem, homeDir) {
565
991
  const path = credentialsPath(homeDir);
@@ -572,7 +998,7 @@ async function loadStoredToken(fileSystem, homeDir) {
572
998
  }
573
999
  }
574
1000
  async function storeToken(fileSystem, homeDir, token) {
575
- const dirPath = join6(homeDir, CREDENTIALS_DIR);
1001
+ const dirPath = join4(homeDir, CREDENTIALS_DIR);
576
1002
  await fileSystem.mkdir(dirPath, { recursive: true });
577
1003
  await fileSystem.writeFile(credentialsPath(homeDir), JSON.stringify({ token }));
578
1004
  }
@@ -694,7 +1120,7 @@ function getLogLines(buffer) {
694
1120
  }
695
1121
 
696
1122
  // lib/bucket/repository.ts
697
- import { join as join7 } from "node:path";
1123
+ import { dirname as dirname2, join as join6 } from "node:path";
698
1124
 
699
1125
  // lib/agent-events.ts
700
1126
  function rawEventToAgentEvent(rawEvent) {
@@ -1116,11 +1542,27 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1116
1542
 
1117
1543
  // lib/cli/commands/loop.ts
1118
1544
  import { spawn as nodeSpawn2 } from "node:child_process";
1545
+ import { readFileSync } from "node:fs";
1546
+ import os from "node:os";
1547
+ import { dirname, join as join5 } from "node:path";
1548
+ import { fileURLToPath } from "node:url";
1549
+
1550
+ // lib/workflow-tasks.ts
1551
+ var IDEA_TRANSITION_PREFIXES = [
1552
+ "Refine Idea: ",
1553
+ "Decompose Idea: ",
1554
+ "Shelve Idea: "
1555
+ ];
1556
+ var BUILD_IDEA_PREFIX = "Build Idea: ";
1557
+ function titleToFilename(title) {
1558
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
1559
+ }
1119
1560
 
1120
1561
  // lib/cli/commands/focus.ts
1121
1562
  function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
1122
1563
  const steps = [];
1123
1564
  let step = 1;
1565
+ const hasIdeaFile = !taskTitle?.startsWith(BUILD_IDEA_PREFIX);
1124
1566
  steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
1125
1567
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
1126
1568
  step++;
@@ -1131,7 +1573,15 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
1131
1573
  step++;
1132
1574
  }
1133
1575
  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.';
1134
- 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)", "", commitMessageLine, "");
1576
+ const commitItems = [
1577
+ " - All implementation changes",
1578
+ " - Deletion of the completed task file",
1579
+ " - Updates to any facts that changed"
1580
+ ];
1581
+ if (hasIdeaFile) {
1582
+ commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
1583
+ }
1584
+ steps.push(`${step}. Create a single atomic commit that includes:`, ...commitItems, "", commitMessageLine, "");
1135
1585
  step++;
1136
1586
  steps.push(`${step}. Push your commit to the remote repository`);
1137
1587
  steps.push("");
@@ -1235,6 +1685,29 @@ async function next(dependencies) {
1235
1685
  }
1236
1686
 
1237
1687
  // lib/cli/commands/loop.ts
1688
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
1689
+ function getDustVersion() {
1690
+ const candidates = [
1691
+ join5(__dirname2, "../../../package.json"),
1692
+ join5(__dirname2, "../package.json")
1693
+ ];
1694
+ for (const candidate of candidates) {
1695
+ try {
1696
+ const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
1697
+ return packageJson.version ?? "unknown";
1698
+ } catch {}
1699
+ }
1700
+ return "unknown";
1701
+ }
1702
+ function getEnvironmentContext(cwd) {
1703
+ return {
1704
+ machineName: os.hostname(),
1705
+ cwd,
1706
+ platform: `${os.platform()} ${os.release()}`,
1707
+ dustVersion: getDustVersion(),
1708
+ runtimeVersion: process.version
1709
+ };
1710
+ }
1238
1711
  function formatLoopEvent(event) {
1239
1712
  switch (event.type) {
1240
1713
  case "loop.warning":
@@ -1351,7 +1824,8 @@ Make sure the repository is in a clean state and synced with remote before finis
1351
1824
  title: "Resolving git conflict",
1352
1825
  prompt: prompt2,
1353
1826
  agentType: "claude",
1354
- purpose: "git-conflict"
1827
+ purpose: "git-conflict",
1828
+ ...getEnvironmentContext(context.cwd)
1355
1829
  });
1356
1830
  try {
1357
1831
  await run2(prompt2, {
@@ -1403,7 +1877,8 @@ ${instructions}`;
1403
1877
  title: task.title ?? task.path,
1404
1878
  prompt,
1405
1879
  agentType: "claude",
1406
- purpose: "task"
1880
+ purpose: "task",
1881
+ ...getEnvironmentContext(context.cwd)
1407
1882
  });
1408
1883
  try {
1409
1884
  await run2(prompt, {
@@ -1511,9 +1986,9 @@ function parseRepository(data) {
1511
1986
  }
1512
1987
  return null;
1513
1988
  }
1514
- function getRepoTempPath(repoName, tempDir) {
1515
- const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
1516
- return join7(tempDir, `dust-bucket-${safeName}`);
1989
+ function getRepoPath(repoName, reposDir) {
1990
+ const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
1991
+ return join6(reposDir, safeName);
1517
1992
  }
1518
1993
  async function cloneRepository(repository, targetPath, spawn, context) {
1519
1994
  return new Promise((resolve) => {
@@ -1654,7 +2129,8 @@ async function addRepository(repository, manager, repoDeps, context) {
1654
2129
  if (manager.repositories.has(repository.name)) {
1655
2130
  return;
1656
2131
  }
1657
- const repoPath = getRepoTempPath(repository.name, repoDeps.getTempDir());
2132
+ const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
2133
+ await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
1658
2134
  if (repoDeps.fileSystem.exists(repoPath)) {
1659
2135
  await removeRepository(repoPath, repoDeps.spawn, context);
1660
2136
  }
@@ -1967,7 +2443,7 @@ function renderTabs(state) {
1967
2443
  const width = name.length + 4;
1968
2444
  if (i === state.selectedIndex) {
1969
2445
  tabs.push({
1970
- text: `${ANSI.INVERSE} ${dot}${ANSI.INVERSE}${color} ${name} ${ANSI.RESET}`,
2446
+ text: ` ${dot}${color} ${ANSI.INVERSE}${name}${ANSI.RESET} `,
1971
2447
  width
1972
2448
  });
1973
2449
  } else {
@@ -2231,6 +2707,13 @@ function createDefaultBucketDependencies() {
2231
2707
  return false;
2232
2708
  }
2233
2709
  },
2710
+ isDirectory: (path) => {
2711
+ try {
2712
+ return statSync(path).isDirectory();
2713
+ } catch {
2714
+ return false;
2715
+ }
2716
+ },
2234
2717
  readFile: (path) => readFile(path, "utf8"),
2235
2718
  writeFile: (path, content) => writeFile(path, content, "utf8"),
2236
2719
  mkdir: (path, options) => mkdir(path, options).then(() => {}),
@@ -2247,7 +2730,7 @@ function createDefaultBucketDependencies() {
2247
2730
  writeStdout: defaultWriteStdout,
2248
2731
  isTTY: process.stdout.isTTY ?? false,
2249
2732
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2250
- getTempDir: () => tmpdir(),
2733
+ getReposDir: () => process.env.DUST_REPOS_DIR || join7(homedir(), ".dust", "repos"),
2251
2734
  auth: {
2252
2735
  createServer: defaultCreateServer,
2253
2736
  openBrowser: defaultOpenBrowser,
@@ -2285,7 +2768,7 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
2285
2768
  run,
2286
2769
  fileSystem,
2287
2770
  sleep: bucketDeps.sleep,
2288
- getTempDir: bucketDeps.getTempDir
2771
+ getReposDir: bucketDeps.getReposDir
2289
2772
  };
2290
2773
  }
2291
2774
  function syncUIWithRepoList(state, repos) {
@@ -2616,22 +3099,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
2616
3099
  }
2617
3100
 
2618
3101
  // lib/cli/commands/lint-markdown.ts
2619
- import { dirname as dirname3, resolve } from "node:path";
2620
-
2621
- // lib/workflow-tasks.ts
2622
- var IDEA_TRANSITION_PREFIXES = [
2623
- "Refine Idea: ",
2624
- "Decompose Idea: ",
2625
- "Shelve Idea: "
2626
- ];
2627
- function titleToFilename(title) {
2628
- return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
2629
- }
2630
-
2631
- // lib/cli/commands/lint-markdown.ts
3102
+ import { dirname as dirname3, join as join8, resolve } from "node:path";
2632
3103
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
2633
3104
  var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
2634
3105
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
3106
+ var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
2635
3107
  var MAX_OPENING_SENTENCE_LENGTH = 150;
2636
3108
  function validateFilename(filePath) {
2637
3109
  const parts = filePath.split("/");
@@ -3085,6 +3557,72 @@ async function safeScanDir(glob, dirPath) {
3085
3557
  throw error;
3086
3558
  }
3087
3559
  }
3560
+ async function validateContentDirectoryFiles(dirPath, fileSystem) {
3561
+ const violations = [];
3562
+ let entries;
3563
+ try {
3564
+ entries = await fileSystem.readdir(dirPath);
3565
+ } catch (error) {
3566
+ if (error.code === "ENOENT") {
3567
+ return [];
3568
+ }
3569
+ throw error;
3570
+ }
3571
+ for (const entry of entries) {
3572
+ const entryPath = `${dirPath}/${entry}`;
3573
+ if (entry.startsWith(".")) {
3574
+ violations.push({
3575
+ file: entryPath,
3576
+ message: `Hidden file "${entry}" found in content directory`
3577
+ });
3578
+ continue;
3579
+ }
3580
+ if (fileSystem.isDirectory(entryPath)) {
3581
+ violations.push({
3582
+ file: entryPath,
3583
+ message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
3584
+ });
3585
+ continue;
3586
+ }
3587
+ if (!entry.endsWith(".md")) {
3588
+ violations.push({
3589
+ file: entryPath,
3590
+ message: `Non-markdown file "${entry}" found in content directory`
3591
+ });
3592
+ }
3593
+ }
3594
+ return violations;
3595
+ }
3596
+ async function validateDirectoryStructure(dustPath, fileSystem, extraDirectories = []) {
3597
+ const violations = [];
3598
+ let entries;
3599
+ try {
3600
+ entries = await fileSystem.readdir(dustPath);
3601
+ } catch (error) {
3602
+ if (error.code === "ENOENT") {
3603
+ return [];
3604
+ }
3605
+ throw error;
3606
+ }
3607
+ const allowedDirectories = new Set([
3608
+ ...EXPECTED_DIRECTORIES,
3609
+ ...extraDirectories
3610
+ ]);
3611
+ for (const entry of entries) {
3612
+ const entryPath = `${dustPath}/${entry}`;
3613
+ if (!fileSystem.isDirectory(entryPath)) {
3614
+ continue;
3615
+ }
3616
+ if (!allowedDirectories.has(entry)) {
3617
+ const allowedList = [...allowedDirectories].sort().join(", ");
3618
+ violations.push({
3619
+ file: entryPath,
3620
+ message: `Unexpected directory "${entry}" in .dust/. Allowed directories: ${allowedList}. To allow this directory, add it to "extraDirectories" in .dust/config/settings.json`
3621
+ });
3622
+ }
3623
+ }
3624
+ return violations;
3625
+ }
3088
3626
  async function lintMarkdown(dependencies) {
3089
3627
  const { context, fileSystem, globScanner: glob } = dependencies;
3090
3628
  const dustPath = `${context.cwd}/.dust`;
@@ -3096,6 +3634,26 @@ async function lintMarkdown(dependencies) {
3096
3634
  }
3097
3635
  const dustFiles = dustScan.files;
3098
3636
  const violations = [];
3637
+ context.stdout("Validating directory structure...");
3638
+ violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
3639
+ const settingsPath = join8(dustPath, "config", "settings.json");
3640
+ if (fileSystem.exists(settingsPath)) {
3641
+ context.stdout("Validating settings.json...");
3642
+ try {
3643
+ const settingsContent = await fileSystem.readFile(settingsPath);
3644
+ const settingsViolations = validateSettingsJson(settingsContent);
3645
+ for (const sv of settingsViolations) {
3646
+ violations.push({
3647
+ file: settingsPath,
3648
+ message: sv.message
3649
+ });
3650
+ }
3651
+ } catch (error) {
3652
+ if (error.code !== "ENOENT") {
3653
+ throw error;
3654
+ }
3655
+ }
3656
+ }
3099
3657
  context.stdout("Validating links in .dust/...");
3100
3658
  for (const file of dustFiles) {
3101
3659
  if (!file.endsWith(".md"))
@@ -3112,6 +3670,10 @@ async function lintMarkdown(dependencies) {
3112
3670
  }
3113
3671
  const contentDirs = ["goals", "facts", "ideas", "tasks"];
3114
3672
  context.stdout("Validating content files...");
3673
+ for (const dir of contentDirs) {
3674
+ const dirPath = `${dustPath}/${dir}`;
3675
+ violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
3676
+ }
3115
3677
  for (const dir of contentDirs) {
3116
3678
  const dirPath = `${dustPath}/${dir}`;
3117
3679
  const { files } = await safeScanDir(glob, dirPath);
@@ -3278,8 +3840,8 @@ async function runValidationCheck(dependencies) {
3278
3840
  });
3279
3841
  const durationMs = Date.now() - startTime;
3280
3842
  return {
3281
- name: "lint markdown",
3282
- command: "dust lint markdown",
3843
+ name: "lint",
3844
+ command: "dust lint",
3283
3845
  exitCode: result.exitCode,
3284
3846
  output: outputLines.join(`
3285
3847
  `),
@@ -3376,7 +3938,45 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
3376
3938
 
3377
3939
  // lib/cli/commands/help.ts
3378
3940
  function generateHelpText(settings) {
3379
- return loadTemplate("help", { bin: settings.dustCommand });
3941
+ const bin = settings.dustCommand;
3942
+ return dedent`
3943
+ 💨 dust - Flow state for AI coding agents.
3944
+
3945
+ Usage: ${bin} <command> [options]
3946
+
3947
+ Commands:
3948
+ init Initialize a new Dust repository
3949
+ lint Run lint checks on .dust/ files
3950
+ list List all items (tasks, ideas, goals, facts)
3951
+ tasks List tasks (actionable work with definitions of done)
3952
+ ideas List ideas (vague proposals, convert to tasks when ready)
3953
+ goals List goals (guiding principles, stable, rarely change)
3954
+ facts List facts (documentation of current system state)
3955
+ next Show tasks ready to work on (not blocked)
3956
+ check Run project-defined quality gate hook
3957
+ agent Agent greeting and routing instructions
3958
+ focus Declare current objective (for remote session tracking)
3959
+ pick task Pick the next task to work on
3960
+ implement task Implement a task
3961
+ new task Create a new task
3962
+ new goal Create a new goal
3963
+ new idea Create a new idea
3964
+ loop claude Run continuous Claude iteration on tasks
3965
+ pre push Git pre-push hook validation
3966
+ help Show this help message
3967
+
3968
+ 🤖 Agent Guide
3969
+
3970
+ Dust is a lightweight planning system. The .dust/ directory contains:
3971
+ - goals/ - Guiding principles (stable, rarely change)
3972
+ - ideas/ - Proposals (convert to tasks when ready)
3973
+ - tasks/ - Actionable work with definitions of done
3974
+ - facts/ - Documentation of current system state
3975
+
3976
+ Workflow: Pick a task → implement it → delete the task file → commit atomically.
3977
+
3978
+ Run \`${bin} agent\` to get started!
3979
+ `;
3380
3980
  }
3381
3981
  async function help(dependencies) {
3382
3982
  dependencies.context.stdout(generateHelpText(dependencies.settings));
@@ -3407,6 +4007,24 @@ var USE_DUST_FACT = `# Use dust for planning
3407
4007
 
3408
4008
  This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
3409
4009
  `;
4010
+ function claudeMdContent(dustCommand) {
4011
+ return dedent`
4012
+ # Claude Code Instructions
4013
+
4014
+ This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4015
+
4016
+ Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4017
+ `;
4018
+ }
4019
+ function agentsMdContent(dustCommand) {
4020
+ return dedent`
4021
+ # Agent Instructions
4022
+
4023
+ This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4024
+
4025
+ Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4026
+ `;
4027
+ }
3410
4028
  async function init(dependencies) {
3411
4029
  const { context, fileSystem } = dependencies;
3412
4030
  const colors = getColors();
@@ -3445,8 +4063,9 @@ async function init(dependencies) {
3445
4063
  }
3446
4064
  const claudeMdPath = `${context.cwd}/CLAUDE.md`;
3447
4065
  try {
3448
- const claudeContent = loadTemplate("claude-md", { dustCommand });
3449
- await fileSystem.writeFile(claudeMdPath, claudeContent, { flag: "wx" });
4066
+ await fileSystem.writeFile(claudeMdPath, claudeMdContent(dustCommand), {
4067
+ flag: "wx"
4068
+ });
3450
4069
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
3451
4070
  } catch (error) {
3452
4071
  if (error.code === "EEXIST") {
@@ -3457,8 +4076,9 @@ async function init(dependencies) {
3457
4076
  }
3458
4077
  const agentsMdPath = `${context.cwd}/AGENTS.md`;
3459
4078
  try {
3460
- const agentsContent = loadTemplate("agents-md", { dustCommand });
3461
- await fileSystem.writeFile(agentsMdPath, agentsContent, { flag: "wx" });
4079
+ await fileSystem.writeFile(agentsMdPath, agentsMdContent(dustCommand), {
4080
+ flag: "wx"
4081
+ });
3462
4082
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
3463
4083
  } catch (error) {
3464
4084
  if (error.code === "EEXIST") {
@@ -3605,23 +4225,147 @@ async function list(dependencies) {
3605
4225
  return { exitCode: 0 };
3606
4226
  }
3607
4227
 
3608
- // lib/cli/template-command.ts
3609
- var createTemplateCommand = (templateName) => async (dependencies) => {
4228
+ // lib/cli/commands/new-goal.ts
4229
+ function newGoalInstructions(vars) {
4230
+ const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
4231
+ return dedent`
4232
+ ## Adding a New Goal
4233
+
4234
+ Goals are guiding principles that persist across tasks. They define the "why" behind the work.
4235
+
4236
+ ${intro}
4237
+ 1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
4238
+ 2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
4239
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
4240
+ 4. Write a clear description explaining:
4241
+ - What this goal means in practice
4242
+ - Why it matters for the project
4243
+ - How to evaluate whether work supports this goal
4244
+ 5. Run \`${vars.bin} lint\` to catch any formatting issues
4245
+ 6. Create a single atomic commit with a message in the format "Add goal: <title>"
4246
+ 7. Push your commit to the remote repository
4247
+
4248
+ Goals should be:
4249
+ - **Stable** - They rarely change once established
4250
+ - **Actionable** - Tasks can be linked to them
4251
+ - **Clear** - Anyone reading should understand what it means
4252
+ `;
4253
+ }
4254
+ async function newGoal(dependencies) {
3610
4255
  const { context, settings } = dependencies;
3611
4256
  const hooksInstalled = await manageGitHooks(dependencies);
3612
4257
  const vars = templateVariables(settings, hooksInstalled);
3613
- context.stdout(loadTemplate(templateName, vars));
4258
+ context.stdout(newGoalInstructions(vars));
3614
4259
  return { exitCode: 0 };
3615
- };
3616
-
3617
- // lib/cli/commands/new-goal.ts
3618
- var newGoal = createTemplateCommand("agent-new-goal");
4260
+ }
3619
4261
 
3620
4262
  // lib/cli/commands/new-idea.ts
3621
- var newIdea = createTemplateCommand("agent-new-idea");
4263
+ function newIdeaInstructions(vars) {
4264
+ return dedent`
4265
+ ## Adding a New Idea
4266
+
4267
+ Follow these steps:
4268
+
4269
+ 1. Run \`${vars.bin} ideas\` to see all existing ideas and avoid duplicates
4270
+ 2. Create a new markdown file in \`.dust/ideas/\` with a descriptive kebab-case name (e.g., \`improve-error-messages.md\`)
4271
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Improve error messages\`)
4272
+ 4. Write a brief description of the potential change or improvement
4273
+ 5. If the idea has open questions, add an \`## Open Questions\` section (see below)
4274
+ 6. Run \`${vars.bin} lint\` to catch any issues with the idea file format
4275
+ 7. Create a single atomic commit with a message in the format "Add idea: <title>"
4276
+ 8. Push your commit to the remote repository
4277
+
4278
+ ### Open Questions section
4279
+
4280
+ 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:
4281
+
4282
+ \`\`\`markdown
4283
+ ## Open Questions
4284
+
4285
+ ### Should we take our own payments?
4286
+
4287
+ #### Yes, take our own payments
4288
+
4289
+ Lower costs and we become the seller of record, but requires a merchant account.
4290
+
4291
+ #### No, use a payment provider
4292
+
4293
+ Higher costs but simpler setup. No merchant account needed.
4294
+
4295
+ ### Which storage backend should we use?
4296
+
4297
+ #### SQLite
4298
+
4299
+ Simple and embedded. Good for single-node deployments.
4300
+
4301
+ #### PostgreSQL
4302
+
4303
+ Scalable but requires a separate server.
4304
+ \`\`\`
4305
+
4306
+ Rules:
4307
+ - Questions are \`###\` headings and must end with \`?\`
4308
+ - Options are \`####\` headings beneath a question
4309
+ - Each question must have at least one option
4310
+ - Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
4311
+ `;
4312
+ }
4313
+ async function newIdea(dependencies) {
4314
+ const { context, settings } = dependencies;
4315
+ const hooksInstalled = await manageGitHooks(dependencies);
4316
+ const vars = templateVariables(settings, hooksInstalled);
4317
+ context.stdout(newIdeaInstructions(vars));
4318
+ return { exitCode: 0 };
4319
+ }
3622
4320
 
3623
4321
  // lib/cli/commands/new-task.ts
3624
- var newTask = createTemplateCommand("agent-new-task");
4322
+ function newTaskInstructions(vars) {
4323
+ const steps = [];
4324
+ steps.push("## Adding a New Task");
4325
+ steps.push("");
4326
+ if (vars.isClaudeCodeWeb) {
4327
+ steps.push("Follow these steps to create the task definition, then spawn a sub-agent for implementation.");
4328
+ steps.push("");
4329
+ steps.push("Use a todo list to track your progress through these steps.");
4330
+ } else {
4331
+ steps.push("Follow these steps:");
4332
+ }
4333
+ steps.push("");
4334
+ steps.push(`1. Run \`${vars.bin} ideas\` to see all existing ideas`);
4335
+ steps.push("2. Determine which ideas (if any) should be:");
4336
+ steps.push(" - **Deleted** - if the new task fully covers the idea");
4337
+ steps.push(" - **Updated** - if the idea's scope changes as a result of the task");
4338
+ steps.push("3. Research thoroughly to ensure the task will be clearly defined:");
4339
+ steps.push(" - Explore the codebase to understand existing patterns and relevant files");
4340
+ steps.push(" - Identify exactly which files need to change and how");
4341
+ steps.push(" - Resolve any ambiguities in the requirements before writing the task");
4342
+ steps.push(" - Gather specific technical details (function names, file paths, data structures)");
4343
+ steps.push(" - The goal is a task description with minimal ambiguity at implementation time");
4344
+ steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
4345
+ steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
4346
+ 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.');
4347
+ steps.push("7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)");
4348
+ steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
4349
+ steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
4350
+ steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
4351
+ steps.push('11. Create a single atomic commit with a message in the format "Add task: <title>" that includes:');
4352
+ steps.push(" - The new task file");
4353
+ steps.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
4354
+ if (vars.isClaudeCodeWeb) {
4355
+ 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\`"`);
4356
+ } else {
4357
+ steps.push("12. Push your commit to the remote repository");
4358
+ }
4359
+ return steps.join(`
4360
+ `);
4361
+ }
4362
+ async function newTask(dependencies) {
4363
+ const { context, settings } = dependencies;
4364
+ const hooksInstalled = await manageGitHooks(dependencies);
4365
+ const vars = templateVariables(settings, hooksInstalled);
4366
+ context.stdout(newTaskInstructions(vars));
4367
+ return { exitCode: 0 };
4368
+ }
3625
4369
 
3626
4370
  // lib/cli/commands/pick-task.ts
3627
4371
  async function pickTask(dependencies) {
@@ -3793,7 +4537,7 @@ async function facts(dependencies) {
3793
4537
  // lib/cli/main.ts
3794
4538
  var commandRegistry = {
3795
4539
  init,
3796
- "lint markdown": lintMarkdown,
4540
+ lint: lintMarkdown,
3797
4541
  list,
3798
4542
  tasks,
3799
4543
  goals,
@@ -3862,6 +4606,13 @@ async function main(options) {
3862
4606
  function createFileSystem(primitives) {
3863
4607
  return {
3864
4608
  exists: primitives.existsSync,
4609
+ isDirectory: (path) => {
4610
+ try {
4611
+ return primitives.statSync(path).isDirectory();
4612
+ } catch {
4613
+ return false;
4614
+ }
4615
+ },
3865
4616
  readFile: (path) => primitives.readFile(path, "utf-8"),
3866
4617
  writeFile: (path, content, options) => primitives.writeFile(path, content, {
3867
4618
  encoding: "utf-8",
@@ -3901,4 +4652,4 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
3901
4652
  }
3902
4653
 
3903
4654
  // lib/cli/run.ts
3904
- 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 });
4655
+ 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 });