@joshski/dust 0.1.108 → 0.1.109

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
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
7
7
  var require_package = __commonJS((exports, module) => {
8
8
  module.exports = {
9
9
  name: "@joshski/dust",
10
- version: "0.1.108",
10
+ version: "0.1.109",
11
11
  description: "Flow state for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
@@ -409,6 +409,16 @@ function createGitDirectoryFileSorter(gitRunner) {
409
409
 
410
410
  // lib/config/settings.ts
411
411
  import { join as join2 } from "node:path";
412
+
413
+ // lib/filesystem/error-codes.ts
414
+ function isErrnoException(error) {
415
+ return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string";
416
+ }
417
+ function isErrorCode(error, code) {
418
+ return isErrnoException(error) && error.code === code;
419
+ }
420
+
421
+ // lib/config/settings.ts
412
422
  var KNOWN_SETTINGS_KEYS = new Set([
413
423
  "dustCommand",
414
424
  "checks",
@@ -693,7 +703,7 @@ async function loadSettings(cwd, fileSystem, runtime) {
693
703
  }
694
704
  return result;
695
705
  } catch (error) {
696
- if (error.code === "ENOENT") {
706
+ if (isErrorCode(error, "ENOENT")) {
697
707
  const result = {
698
708
  dustCommand: detectDustCommand(cwd, fileSystem, runtime)
699
709
  };
@@ -711,7 +721,7 @@ async function loadSettings(cwd, fileSystem, runtime) {
711
721
  }
712
722
 
713
723
  // lib/version.ts
714
- var DUST_VERSION = "0.1.108";
724
+ var DUST_VERSION = "0.1.109";
715
725
 
716
726
  // lib/cli/middleware.ts
717
727
  function applyMiddleware(middlewares, execute) {
@@ -834,7 +844,7 @@ function createHooksManager(cwd, fileSystem, settings) {
834
844
  const content = await fileSystem.readFile(prePushPath);
835
845
  return content.includes(DUST_HOOK_START);
836
846
  } catch (error) {
837
- if (error.code === "ENOENT") {
847
+ if (isErrorCode(error, "ENOENT")) {
838
848
  return false;
839
849
  }
840
850
  throw error;
@@ -862,7 +872,7 @@ ${hookContent}
862
872
  `;
863
873
  }
864
874
  } catch (error) {
865
- if (error.code === "ENOENT") {
875
+ if (isErrorCode(error, "ENOENT")) {
866
876
  finalContent = `#!/bin/sh
867
877
 
868
878
  ${hookContent}
@@ -884,7 +894,7 @@ ${hookContent}
884
894
  const match = dustSection.match(/^(.+) pre push$/m);
885
895
  return match ? match[1] : null;
886
896
  } catch (error) {
887
- if (error.code === "ENOENT") {
897
+ if (isErrorCode(error, "ENOENT")) {
888
898
  return null;
889
899
  }
890
900
  throw error;
@@ -895,7 +905,7 @@ ${hookContent}
895
905
  try {
896
906
  content = await fileSystem.readFile(prePushPath);
897
907
  } catch (error) {
898
- if (error.code === "ENOENT") {
908
+ if (isErrorCode(error, "ENOENT")) {
899
909
  return;
900
910
  }
901
911
  throw error;
@@ -929,7 +939,7 @@ async function loadAgentInstructions(cwd, fileSystem, agentType) {
929
939
  const content = await fileSystem.readFile(instructionsPath);
930
940
  return content.trim();
931
941
  } catch (error) {
932
- if (error.code === "ENOENT") {
942
+ if (isErrorCode(error, "ENOENT")) {
933
943
  return "";
934
944
  }
935
945
  throw error;
@@ -1530,6 +1540,72 @@ function deadCode() {
1530
1540
  - No changes to files outside \`.dust/\`
1531
1541
  `;
1532
1542
  }
1543
+ function directoryHierarchy() {
1544
+ return dedent`
1545
+ # Directory Hierarchy
1546
+
1547
+ Review directory structure and create improvement ideas.
1548
+
1549
+ ${ideasHint}
1550
+
1551
+ ## Scope
1552
+
1553
+ Analyze the project's directory organization for these issues:
1554
+
1555
+ 1. **Concern mixing** - Directories containing files that serve different purposes
1556
+ 2. **Missing grouping** - Related files scattered across multiple locations
1557
+ 3. **Depth inconsistency** - Similar directories at inconsistent depths
1558
+ 4. **Naming inconsistency** - Directory names that don't follow established patterns
1559
+ 5. **Singleton directories** - Directories with only a single file or subdirectory
1560
+ 6. **Orphaned files** - Files at inappropriate directory levels
1561
+
1562
+ ## Analysis Steps
1563
+
1564
+ 1. **Explore the directory tree** - Walk the project's file system recursively, excluding \`node_modules\`, \`.git\`, \`dist\`, \`build\`, \`coverage\`, and other common build artifact directories
1565
+ 2. **Identify issues** - For each of the issue types listed above, look for concrete examples in the directory structure
1566
+ 3. **Create ideas** - For each issue found, create an idea file in \`.dust/ideas/\` with:
1567
+ - Descriptive filename based on the issue type and affected paths
1568
+ - The specific paths affected
1569
+ - Why the current structure is problematic
1570
+ - A proposed reorganization
1571
+ - Migration complexity estimate (low/medium/high)
1572
+
1573
+ ## Output Format
1574
+
1575
+ Each idea file should follow this structure:
1576
+
1577
+ \`\`\`markdown
1578
+ # [Issue Type]: [Brief Description]
1579
+
1580
+ ## Current Structure
1581
+
1582
+ [List specific paths from affectedPaths]
1583
+
1584
+ ## Problem
1585
+
1586
+ [Description of why this is problematic]
1587
+
1588
+ ## Proposed Solution
1589
+
1590
+ [Suggested reorganization]
1591
+
1592
+ ## Migration Complexity
1593
+
1594
+ [Low/Medium/High with brief rationale]
1595
+ \`\`\`
1596
+
1597
+ ## Blocked By
1598
+
1599
+ (none)
1600
+
1601
+ ## Definition of Done
1602
+
1603
+ - Explored the directory tree excluding standard build/tool directories
1604
+ - Created idea files for all findings in \`.dust/ideas/\`
1605
+ - Each idea includes specific paths, problem description, proposed solution, and complexity
1606
+ - No changes to files outside \`.dust/\`
1607
+ `;
1608
+ }
1533
1609
  function documentationDrift() {
1534
1610
  return dedent`
1535
1611
  # Documentation Drift
@@ -1680,6 +1756,110 @@ function factsVerification() {
1680
1756
  - No changes to files outside \`.dust/\`
1681
1757
  `;
1682
1758
  }
1759
+ function factsExpansion() {
1760
+ return dedent`
1761
+ # Facts Expansion
1762
+
1763
+ Review the codebase for significant facts that should be documented in \`.dust/facts/\`.
1764
+
1765
+ ${ideasHint}
1766
+
1767
+ ## Context
1768
+
1769
+ Facts capture how things work today, providing context for agents and contributors. However, not all significant aspects of the codebase are currently documented as facts. This creates gaps where agents working in specific areas may lack important context that isn't obvious from scanning code or having prior framework knowledge.
1770
+
1771
+ ## Applicability
1772
+
1773
+ This audit applies to all codebases. If \`.dust/facts/\` does not exist, the audit will identify initial facts to document.
1774
+
1775
+ ## Scope
1776
+
1777
+ Analyze the codebase for undocumented facts across these areas:
1778
+
1779
+ ### Architectural Decisions
1780
+ - Separation of concerns patterns not enforced by directory structure
1781
+ - Dependency flow rules (e.g., what can depend on what)
1782
+ - Layer boundaries and their purposes
1783
+ - Module initialization order requirements
1784
+ - Plugin or extension mechanisms
1785
+
1786
+ ### Implementation Conventions
1787
+ - Naming patterns for specific types of code (factories, builders, validators)
1788
+ - Error handling conventions (when to throw vs return errors)
1789
+ - Async/await patterns and Promise handling
1790
+ - Resource cleanup patterns
1791
+ - State management approaches
1792
+
1793
+ ### External Integration Points
1794
+ - CLI command structure and parsing approach
1795
+ - Event emission patterns
1796
+ - File system conventions
1797
+ - Process spawning patterns
1798
+ - Network communication protocols
1799
+
1800
+ ### Performance Characteristics
1801
+ - Known performance bottlenecks
1802
+ - Caching strategies
1803
+ - Lazy loading patterns
1804
+ - Resource pooling approaches
1805
+ - Optimization trade-offs
1806
+
1807
+ ### Historical Context
1808
+ - Migration paths from previous approaches
1809
+ - Deprecated patterns still present in legacy code
1810
+ - Trade-offs made in past decisions
1811
+ - Features that were removed and why
1812
+
1813
+ ## Analysis Approach
1814
+
1815
+ 1. **Scan for patterns** - Look for repeated implementation patterns across multiple files
1816
+ 2. **Identify conventions** - Find coding conventions that aren't enforced by linters
1817
+ 3. **Review configuration** - Document configuration systems and their purposes
1818
+ 4. **Trace data flows** - Identify how data moves through the system
1819
+ 5. **Check existing facts** - Compare findings against what's already documented in \`.dust/facts/\`
1820
+ 6. **Filter for significance** - Only suggest facts that would genuinely help future agents (facts that aren't obvious from code inspection or general framework knowledge)
1821
+
1822
+ ## Significance Criteria
1823
+
1824
+ A fact is worth documenting if:
1825
+ - It's not obvious from reading the code in isolation
1826
+ - It represents a project-specific decision or convention
1827
+ - Future agents would benefit from knowing it before making changes
1828
+ - It documents framework patterns actually used in this project
1829
+
1830
+ ## Output Format
1831
+
1832
+ For each suggested fact, create an idea file in \`.dust/ideas/\` that includes:
1833
+
1834
+ ### Fact Title
1835
+ A clear, concise title for the proposed fact.
1836
+
1837
+ ### Why This Matters
1838
+ Explanation of why this fact would be valuable to document (what gaps it fills, what problems it prevents).
1839
+
1840
+ ### What to Document
1841
+ Specific aspects to cover in the fact file.
1842
+
1843
+ ### Where to Look
1844
+ File paths or code locations that demonstrate this fact.
1845
+
1846
+ ### Example Content
1847
+ A sketch of what the fact file might contain (2-3 sentences showing the style and key points).
1848
+
1849
+ ## Blocked By
1850
+
1851
+ (none)
1852
+
1853
+ ## Definition of Done
1854
+
1855
+ - Analyzed codebase for undocumented patterns across all specified areas
1856
+ - Compared findings against existing facts in \`.dust/facts/\`
1857
+ - Applied significance criteria to filter suggestions
1858
+ - Created idea files for each suggested fact with complete metadata
1859
+ - Each idea includes: fact title, why it matters, what to document, where to look, example content
1860
+ - No changes to files outside \`.dust/\`
1861
+ `;
1862
+ }
1683
1863
  function feedbackLoopSpeed() {
1684
1864
  return dedent`
1685
1865
  # Feedback Loop Speed
@@ -1690,7 +1870,7 @@ function feedbackLoopSpeed() {
1690
1870
 
1691
1871
  ## Context
1692
1872
 
1693
- The [Fast Feedback Loops](../principles/fast-feedback-loops.md) principle emphasizes that the primary feedback loop—write code, run checks, see results—should be as fast as possible. Agents especially benefit because they operate in tight loops of change-and-verify; slow feedback wastes tokens and context window space on waiting rather than working.
1873
+ The primary feedback loop—write code, run checks, see results—should be as fast as possible. Agents especially benefit because they operate in tight loops of change-and-verify; slow feedback wastes tokens and context window space on waiting rather than working.
1694
1874
 
1695
1875
  This audit focuses specifically on measuring the development feedback loop speed to help identify which checks consume the most time.
1696
1876
 
@@ -2136,6 +2316,223 @@ function ideasFromPrinciples() {
2136
2316
  - No changes to files outside \`.dust/\`
2137
2317
  `;
2138
2318
  }
2319
+ function incidentalTestDetails() {
2320
+ return dedent`
2321
+ # Incidental Test Details
2322
+
2323
+ Identify tests with overly specific data and other incidental details that obscure test intent.
2324
+
2325
+ ${ideasHint}
2326
+
2327
+ ## Context
2328
+
2329
+ Test clarity suffers when tests include incidental complexity — details that aren't relevant to what's being tested. Overly specific data, unused properties, magic numbers, excessive mocking, and complex nested structures all make tests harder to understand and maintain. This audit identifies these patterns as candidates for simplification.
2330
+
2331
+ The audit flags patterns for review without making judgments about whether they're necessary in specific cases. Some tests legitimately need complex setup to verify specific behaviors; the goal is to surface candidates so agents can evaluate each case and simplify where appropriate.
2332
+
2333
+ ## Guidance
2334
+
2335
+ ### Readable Test Data
2336
+
2337
+ Test data setup should use natural structures that mirror what they represent.
2338
+
2339
+ When test data is easy to read, tests become self-documenting. A file system hierarchy expressed as a nested object immediately conveys structure, while a flat Map with path strings requires mental parsing to understand the relationships.
2340
+
2341
+ Prefer literal structures that visually match the domain:
2342
+
2343
+ \`\`\`javascript
2344
+ // Avoid: flat paths that obscure hierarchy
2345
+ const fs = createFileSystemEmulator({
2346
+ files: new Map([['/project/.dust/principles/my-goal.md', '# My Goal']]),
2347
+ existingPaths: new Set(['/project/.dust/ideas']),
2348
+ })
2349
+
2350
+ // Prefer: nested object that mirrors file system structure
2351
+ const fs = createFileSystemEmulator({
2352
+ project: {
2353
+ '.dust': {
2354
+ principles: {
2355
+ 'my-goal.md': '# My Goal'
2356
+ },
2357
+ ideas: {}
2358
+ }
2359
+ }
2360
+ })
2361
+ \`\`\`
2362
+
2363
+ The nested form:
2364
+ - Shows parent-child relationships through indentation
2365
+ - Makes empty directories explicit with empty objects
2366
+ - Requires no mental path concatenation to understand structure
2367
+
2368
+ ### Comprehensive Assertions
2369
+
2370
+ Assert the whole, not the parts.
2371
+
2372
+ When you break a complex object into many small assertions, a failure tells you *one thing that's wrong*. When you assert against the whole expected value, the diff tells you *what actually happened versus what you expected* — the full picture, in one glance.
2373
+
2374
+ Small assertions are like yes/no questions to a witness. A whole-object assertion is like asking "tell me what you saw."
2375
+
2376
+ Collapse multiple partial assertions into one comprehensive assertion:
2377
+
2378
+ \`\`\`javascript
2379
+ // Fragmented — each failure is a narrow keyhole
2380
+ expect(result.name).toBe("Alice");
2381
+ expect(result.age).toBe(30);
2382
+ expect(result.role).toBe("admin");
2383
+
2384
+ // Whole — a failure diff tells the full story
2385
+ expect(result).toEqual({
2386
+ name: "Alice",
2387
+ age: 30,
2388
+ role: "admin",
2389
+ });
2390
+ \`\`\`
2391
+
2392
+ If \`role\` is \`"user"\` and \`age\` is \`29\`, the fragmented version stops at the first failure. The whole-object assertion shows both discrepancies at once, in context.
2393
+
2394
+ ### Self-Diagnosing Tests
2395
+
2396
+ When a big test fails, it should be self-evident how to diagnose and fix the failure.
2397
+
2398
+ The more moving parts a test has — end-to-end, system, integration — the more critical this becomes. A test that fails with \`expected true, received false\` forces the developer (or agent) to re-run, add logging, and guess. A test that fails with a rich diff showing the actual state versus the expected state turns diagnosis into reading.
2399
+
2400
+ Anti-patterns:
2401
+
2402
+ **Boolean flattening** — collapsing a rich value into true/false before asserting:
2403
+ \`\`\`javascript
2404
+ // Bad: "expected true, received false" — what events arrived?
2405
+ expect(events.some(e => e.type === 'check-passed')).toBe(true)
2406
+
2407
+ // Good: shows the actual event types on failure
2408
+ expect(events.map(e => e.type)).toContain('check-passed')
2409
+ \`\`\`
2410
+
2411
+ **Length-only assertions** — checking count without showing contents:
2412
+ \`\`\`javascript
2413
+ // Bad: "expected 2, received 0" — what requests were captured?
2414
+ expect(requests.length).toBe(2)
2415
+
2416
+ // Good: shows the actual requests on failure
2417
+ expect(requests).toHaveLength(2) // vitest shows the array
2418
+ \`\`\`
2419
+
2420
+ **Silent guards** — using \`if\` where an assertion belongs:
2421
+ \`\`\`javascript
2422
+ // Bad: silently passes when settings is undefined
2423
+ if (settings) {
2424
+ expect(JSON.parse(settings).key).toBeDefined()
2425
+ }
2426
+
2427
+ // Good: fails explicitly if settings is missing
2428
+ expect(settings).toBeDefined()
2429
+ const parsed = JSON.parse(settings!)
2430
+ expect(parsed.key).toBeDefined()
2431
+ \`\`\`
2432
+
2433
+ ### Functional Core, Imperative Shell
2434
+
2435
+ Separate code into a pure "functional core" and a thin "imperative shell." The core takes values in and returns values out, with no side effects. The shell handles I/O and wires things together.
2436
+
2437
+ Purely functional code makes some things easier to understand: because values don't change, you can call functions and know that only their return value matters—they don't change anything outside themselves.
2438
+
2439
+ The functional core contains business logic as pure functions that take values and return values. The imperative shell sits at the boundary, reading input, calling into the core, and performing side effects with the results. This keeps the majority of code easy to test (no mocks or stubs needed for pure functions) and makes the I/O surface area small and explicit.
2440
+
2441
+ ## Scope
2442
+
2443
+ Search for test files and analyze them for clarity issues:
2444
+
2445
+ 1. **Test files** - Files matching \`*.test.ts\`, \`*.test.js\`, \`*.spec.ts\`, \`*.spec.js\`
2446
+ - Include unit, integration, and system tests
2447
+ - Exclude exploratory test files
2448
+
2449
+ 2. **Patterns to identify**:
2450
+ - Object literals with unused properties in test setup
2451
+ - Magic numbers without semantic meaning
2452
+ - Excessive mock/stub setup
2453
+ - Complex nested structures where simpler ones would suffice
2454
+ - Brittle string assertions coupled to formatting
2455
+ - Boolean flattening (testing \`.toBe(true)\` instead of showing actual values)
2456
+ - Length-only assertions (testing \`.length\` instead of \`.toHaveLength()\`)
2457
+ - Silent guards (using \`if\` where assertions belong)
2458
+
2459
+ ## Analysis Steps
2460
+
2461
+ 1. **Find test files**
2462
+ - Search for \`**/*.test.ts\`, \`**/*.test.js\`, \`**/*.spec.ts\`, \`**/*.spec.js\`
2463
+ - Filter out exploratory tests
2464
+
2465
+ 2. **Analyze each test file**
2466
+ - Look for object literals in test setup with properties that aren't used in assertions
2467
+ - Identify numeric literals that lack semantic meaning (e.g., \`42\`, \`123\` without explaining what they represent)
2468
+ - Count mock/stub setup lines relative to actual test logic
2469
+ - Check for deeply nested test data structures (3+ levels)
2470
+ - Find string assertions that compare exact formatting (spaces, newlines, etc.) rather than semantic content
2471
+ - Detect boolean flattening patterns (\`.some()\`, \`.every()\`, \`.includes()\` followed by \`.toBe(true/false)\`)
2472
+ - Find length checks using \`.length\` property instead of \`.toHaveLength()\`
2473
+ - Locate conditional logic in tests (\`if\` statements) that should be assertions
2474
+
2475
+ 3. **Create ideas for issues found**
2476
+ - Group issues by test file
2477
+ - For each file with issues, create an idea file documenting:
2478
+ - Test file path
2479
+ - List of patterns found with line numbers
2480
+ - Pattern categories
2481
+ - Current problematic patterns
2482
+ - Recommended refactoring approaches
2483
+
2484
+ ## Output Format
2485
+
2486
+ For each test file with clarity issues, create an idea file with:
2487
+
2488
+ ### Title
2489
+ "Simplify test data in [filename]"
2490
+
2491
+ ### Content Structure
2492
+ \`\`\`markdown
2493
+ # Simplify test data in [filename]
2494
+
2495
+ The test file \`[path]\` contains incidental details that obscure test intent.
2496
+
2497
+ ## Issues Found
2498
+
2499
+ ### [Pattern Name] (line X)
2500
+ - **Current**: \`[code snippet]\`
2501
+ - **Issue**: [explanation of how this obscures intent]
2502
+ - **Recommendation**: [specific simplification guidance]
2503
+
2504
+ [Repeat for each issue]
2505
+ \`\`\`
2506
+
2507
+ ## Applicability
2508
+
2509
+ This audit applies to codebases with test files. If the codebase has no test files (\`*.test.ts\`, \`*.spec.js\`, etc.), document that finding and skip the detailed analysis.
2510
+
2511
+ ## Focus
2512
+
2513
+ This audit focuses purely on test clarity — whether tests clearly communicate intent. It does not evaluate test performance or execution speed.
2514
+
2515
+ ## Blocked By
2516
+
2517
+ (none)
2518
+
2519
+ ## Definition of Done
2520
+
2521
+ - Searched for all test files in the codebase
2522
+ - Analyzed test files for incidental complexity patterns
2523
+ - Identified tests with unused properties in setup data
2524
+ - Found magic numbers lacking semantic meaning
2525
+ - Flagged excessive mock/stub setup
2526
+ - Located complex nested structures
2527
+ - Detected brittle string assertions
2528
+ - Found boolean flattening patterns
2529
+ - Located length-only assertions
2530
+ - Identified silent guards (if statements in tests)
2531
+ - Created idea files for each test file with findings
2532
+ - Each idea includes: file path, issues with line numbers, pattern categories, current patterns, recommendations
2533
+ - No changes to files outside \`.dust/\`
2534
+ `;
2535
+ }
2139
2536
  function commitReview() {
2140
2537
  return dedent`
2141
2538
  # Commit Review
@@ -2146,7 +2543,13 @@ function commitReview() {
2146
2543
 
2147
2544
  ## Scope
2148
2545
 
2149
- Analyze commits since the last commit-review audit (check \`.dust/done/\` for previous runs). Focus on these signals:
2546
+ Determine which commits to analyze:
2547
+
2548
+ 1. Check VCS history for a prior commit-review run: \`git log --grep="Audit: Commit Review" -1 --format=%H\`
2549
+ 2. If found, analyze commits since that commit
2550
+ 3. If not found, analyze the last 20 commits as a fallback
2551
+
2552
+ Focus on these signals:
2150
2553
 
2151
2554
  1. **File churn** - Files modified frequently across multiple commits may have unclear responsibilities or be accumulating technical debt
2152
2555
  2. **Size growth** - Files that have grown significantly may benefit from decomposition
@@ -2763,7 +3166,7 @@ function testAssertions() {
2763
3166
 
2764
3167
  ## Background
2765
3168
 
2766
- The [Comprehensive Assertions](../principles/comprehensive-assertions.md) principle covers asserting whole objects rather than fragments. The [Self-Diagnosing Tests](../principles/self-diagnosing-tests.md) principle covers making failure messages informative. This audit addresses complementary assertion quality signals not covered by existing principles.
3169
+ Comprehensive assertions (asserting the whole, not the parts) provide richer failure diagnostics. Self-diagnosing tests ensure that failures reveal enough context to guide a fix without re-running. This audit addresses complementary assertion quality signals not covered by those principles.
2767
3170
 
2768
3171
  ## Scope
2769
3172
 
@@ -2822,7 +3225,7 @@ function testAssertions() {
2822
3225
  - Require test updates for unrelated changes
2823
3226
  - Obscure what the test is actually verifying
2824
3227
 
2825
- This works in tension with [Comprehensive Assertions](../principles/comprehensive-assertions.md). Let context determine the balance:
3228
+ This works in tension with comprehensive assertions (asserting the whole, not the parts). Let context determine the balance:
2826
3229
  - Public API contracts → comprehensive assertions
2827
3230
  - Internal implementation tests → precise assertions
2828
3231
  - Snapshot tests → consider \`toMatchSnapshot()\` with care
@@ -2849,7 +3252,7 @@ function testAssertions() {
2849
3252
 
2850
3253
  Tests should ideally verify one behavior or scenario. When a test has multiple unrelated assertions, a failure in the first masks all subsequent ones.
2851
3254
 
2852
- This does not mean "one \`expect\` call per test". A single logical assertion may require multiple \`expect\` calls to express (especially for complex state). The [Comprehensive Assertions](../principles/comprehensive-assertions.md) principle often allows collapsing multiple calls into one whole-object assertion.
3255
+ This does not mean "one \`expect\` call per test". A single logical assertion may require multiple \`expect\` calls to express (especially for complex state). Comprehensive assertions (asserting the whole, not the parts) often allow collapsing multiple calls into one whole-object assertion.
2853
3256
 
2854
3257
  The anti-pattern to avoid:
2855
3258
  \`\`\`javascript
@@ -3012,6 +3415,95 @@ function loggingAndTraceability() {
3012
3415
  - No changes to files outside \`.dust/\`
3013
3416
  `;
3014
3417
  }
3418
+ function testDeterminism() {
3419
+ return dedent`
3420
+ # Test Determinism
3421
+
3422
+ Audit unit tests for non-deterministic patterns that cause tests to produce inconsistent results across different environments or executions.
3423
+
3424
+ ${ideasHint}
3425
+
3426
+ ## Context
3427
+
3428
+ Tests must produce the same result regardless of where they run. Non-deterministic tests undermine confidence in CI, make debugging harder, and waste developer time chasing phantom failures. This audit identifies patterns that introduce non-determinism: time dependencies, randomness, environment variable access, filesystem operations, real timers, and platform-specific behavior.
3429
+
3430
+ ## Scope
3431
+
3432
+ Search for unit test files and analyze them for determinism issues:
3433
+
3434
+ 1. **Unit test files** - Files matching \`*.test.ts\`, \`*.test.js\`, \`*.spec.ts\`, \`*.spec.js\`
3435
+ - Exclude system test files (files containing 'system-test' or in 'system-tests/' directories)
3436
+ - Exclude exploratory test files
3437
+
3438
+ 2. **Issue categories to detect**:
3439
+ - Time dependencies (\`Date.now()\`, \`new Date()\`) — should use dependency injection or stubbed time
3440
+ - Randomness (\`Math.random()\`, \`crypto.randomBytes()\`, \`randomUUID()\`) — should use seeded random or injection
3441
+ - Environment variables (\`process.env.VARIABLE\` without \`stubEnv\`) — should use \`stubEnv()\` or pass env as a parameter
3442
+ - Filesystem operations (file reads/writes in unit tests) — should use in-memory filesystem or ensure cleanup
3443
+ - Real timers (\`setTimeout\`, \`setInterval\` without fake timers) — should use \`vi.useFakeTimers()\`
3444
+ - Platform-specific code (\`process.platform\`, \`__dirname\`, \`os.EOL\`) — should use dependency injection or normalize paths
3445
+
3446
+ ## Analysis Steps
3447
+
3448
+ 1. **Find unit test files**
3449
+ - Search for \`**/*.test.ts\`, \`**/*.test.js\`, \`**/*.spec.ts\`, \`**/*.spec.js\`
3450
+ - Filter out system test files and exploratory tests
3451
+
3452
+ 2. **Analyze each test file**
3453
+ - Read the file content
3454
+ - Look for the patterns listed above
3455
+ - Note: patterns used inside stub/mock setups (\`vi.fn()\`, \`vi.mock()\`, \`vi.spyOn()\`), function parameter type annotations, or \`stubEnv()\` calls are not issues — they represent proper test practices
3456
+
3457
+ 3. **Create ideas for issues found**
3458
+ - Group issues by test file
3459
+ - For each file with issues, create an idea file documenting:
3460
+ - Test file path
3461
+ - List of issues with line numbers
3462
+ - Issue categories
3463
+ - Current problematic patterns
3464
+ - Recommended refactoring approaches
3465
+
3466
+ ## Output Format
3467
+
3468
+ For each test file with determinism issues, create an idea file with:
3469
+
3470
+ ### Title
3471
+ "Refactor [filename] for test determinism"
3472
+
3473
+ ### Content Structure
3474
+ \`\`\`markdown
3475
+ # Refactor [filename] for test determinism
3476
+
3477
+ The test file \`[path]\` contains non-deterministic patterns that should be refactored.
3478
+
3479
+ ## Issues Found
3480
+
3481
+ ### [Category Name] (line X)
3482
+ - **Pattern**: \`[code snippet]\`
3483
+ - **Issue**: [explanation of why this is non-deterministic]
3484
+ - **Recommendation**: [specific refactoring guidance]
3485
+
3486
+ [Repeat for each issue]
3487
+ \`\`\`
3488
+
3489
+ ## Applicability
3490
+
3491
+ This audit applies to codebases with unit tests. If the codebase has no unit test files (\`*.test.ts\`, \`*.spec.js\`, etc.), document that finding and skip the detailed analysis.
3492
+
3493
+ ## Blocked By
3494
+
3495
+ (none)
3496
+
3497
+ ## Definition of Done
3498
+
3499
+ - Searched for unit test files (\`*.test.ts\`, \`*.test.js\`, \`*.spec.ts\`, \`*.spec.js\`)
3500
+ - Excluded system test and exploratory test files
3501
+ - Analyzed each unit test file for determinism issues
3502
+ - Created idea files for test files containing determinism issues
3503
+ - Each idea includes specific line numbers, patterns, and refactoring guidance
3504
+ - No changes to files outside \`.dust/\`
3505
+ `;
3506
+ }
3015
3507
  function testPyramid() {
3016
3508
  return dedent`
3017
3509
  # Test Pyramid
@@ -3380,7 +3872,7 @@ function ciDevelopmentParity() {
3380
3872
  2. **Wasted cycles** - Developers push code that passes locally only to have CI fail
3381
3873
  3. **Agent confusion** - AI agents rely on consistent feedback; discrepancies trigger incorrect debugging paths
3382
3874
 
3383
- The [Reproducible Checks](../principles/reproducible-checks.md) principle ensures the same checks run everywhere.
3875
+ Every check must produce the same result regardless of who runs it, when, or on what machine.
3384
3876
 
3385
3877
  ## Scope
3386
3878
 
@@ -3470,7 +3962,7 @@ function ciDevelopmentParity() {
3470
3962
 
3471
3963
  - Developers may push code that passes locally but fails CI on other checks
3472
3964
  - CI provides no coverage for [check category]
3473
- - The [Stop the Line](../principles/stop-the-line.md) principle is violated - problems aren't caught before merge
3965
+ - Problems aren't caught before merge—any worker should halt and fix a problem the moment they detect it
3474
3966
 
3475
3967
  ## Suggested Fix
3476
3968
 
@@ -3494,7 +3986,7 @@ function ciDevelopmentParity() {
3494
3986
  ## Impact
3495
3987
 
3496
3988
  - Developers don't get [check category] feedback until CI runs
3497
- - [Fast Feedback Loops](../principles/fast-feedback-loops.md) are broken - local checks give incomplete picture
3989
+ - Fast feedback loops are brokenlocal checks give incomplete picture
3498
3990
  - Agents may make changes that pass local checks but fail CI
3499
3991
 
3500
3992
  ## Suggested Fix
@@ -3532,7 +4024,7 @@ function commitMessageQuality() {
3532
4024
 
3533
4025
  ## Context
3534
4026
 
3535
- The [Traceable Decisions](../principles/traceable-decisions.md) principle emphasizes that commit history should explain why changes were made. Good commit messages help agents understand project history and make better decisions. This audit evaluates commit message quality itself, not the code changes.
4027
+ Commit history should explain why changes were made, not just what changed. Good commit messages help agents understand project history and make better decisions. This audit evaluates commit message quality itself, not the code changes.
3536
4028
 
3537
4029
  ## Scope
3538
4030
 
@@ -3673,20 +4165,14 @@ function commitMessageQuality() {
3673
4165
  `;
3674
4166
  }
3675
4167
  function suggestAudits() {
3676
- const auditList = Object.entries(stockAuditFunctions).filter(([name]) => name !== "suggest-audits").toSorted(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
3677
- const template = render();
3678
- const description = extractOpeningSentence(template);
3679
- return `- **${name}**: ${description}`;
3680
- }).join(`
3681
- `);
3682
- let content = dedent`
4168
+ return dedent`
3683
4169
  # Suggest Audits
3684
4170
 
3685
4171
  Analyze recent commits and create tasks for relevant audits to run.
3686
4172
 
3687
4173
  ## Context
3688
4174
 
3689
- This audit examines recent commit history and suggests which stock audits would be valuable based on what changed. Rather than manually selecting audits, this provides an automated way to maintain codebase health by matching recent work to appropriate audits.
4175
+ This audit examines recent commit history and suggests which audits would be valuable based on what changed. Rather than manually selecting audits, this provides an automated way to maintain codebase health by matching recent work to appropriate audits.
3690
4176
 
3691
4177
  ## Commit Range
3692
4178
 
@@ -3698,21 +4184,17 @@ function suggestAudits() {
3698
4184
 
3699
4185
  ## Available Audits
3700
4186
 
3701
- `;
3702
- content += `
3703
-
3704
- ` + auditList + `
3705
- `;
3706
- content += dedent`
4187
+ Run \`dust audit\` to list all available audits (including both stock audits and any repository-specific audits configured in \`.dust/config/audits/\`). This will show the audit name and description for each available audit.
3707
4188
 
3708
4189
  ## Analysis Steps
3709
4190
 
3710
- 1. **Gather commits** - Get the list of commits in the determined range with their messages and changed files
3711
- 2. **Categorize changes** - Group commits by the type of work (features, fixes, refactoring, tests, docs, config)
3712
- 3. **Match to audits** - For each relevant audit, explain why recent changes make it valuable:
4191
+ 1. **List audits** - Run \`dust audit\` to get the complete list of available audits with descriptions
4192
+ 2. **Gather commits** - Get the list of commits in the determined range with their messages and changed files
4193
+ 3. **Categorize changes** - Group commits by the type of work (features, fixes, refactoring, tests, docs, config)
4194
+ 4. **Match to audits** - For each relevant audit, explain why recent changes make it valuable:
3713
4195
  - What specific commits or file changes triggered the suggestion?
3714
4196
  - What might the audit uncover given this context?
3715
- 4. **Create tasks** - For each suggested audit, create a task file in \`.dust/tasks/\`
4197
+ 5. **Create tasks** - For each suggested audit, create a task file in \`.dust/tasks/\`
3716
4198
 
3717
4199
  ## Output
3718
4200
 
@@ -3761,7 +4243,6 @@ function suggestAudits() {
3761
4243
  - Each task explains why the audit is valuable given recent changes
3762
4244
  - No changes to files outside \`.dust/\`
3763
4245
  `;
3764
- return content;
3765
4246
  }
3766
4247
  var stockAuditFunctions = {
3767
4248
  "agent-developer-experience": agentDeveloperExperience,
@@ -3777,7 +4258,9 @@ var stockAuditFunctions = {
3777
4258
  "data-access-review": dataAccessReview,
3778
4259
  "dead-code": deadCode,
3779
4260
  "design-patterns": designPatterns,
4261
+ "directory-hierarchy": directoryHierarchy,
3780
4262
  "error-handling": errorHandling,
4263
+ "facts-expansion": factsExpansion,
3781
4264
  "facts-verification": factsVerification,
3782
4265
  "feedback-loop-speed": feedbackLoopSpeed,
3783
4266
  "flaky-tests": flakyTests,
@@ -3785,6 +4268,7 @@ var stockAuditFunctions = {
3785
4268
  "commit-review": commitReview,
3786
4269
  "ideas-from-principles": ideasFromPrinciples,
3787
4270
  "idiomatic-style": idiomaticStyle,
4271
+ "incidental-test-details": incidentalTestDetails,
3788
4272
  "logging-and-traceability": loggingAndTraceability,
3789
4273
  "primitive-obsession": primitiveObsession,
3790
4274
  "repository-context": repositoryContext,
@@ -3794,6 +4278,7 @@ var stockAuditFunctions = {
3794
4278
  "stale-ideas": staleIdeas,
3795
4279
  "suggest-audits": suggestAudits,
3796
4280
  "test-assertions": testAssertions,
4281
+ "test-determinism": testDeterminism,
3797
4282
  "test-pyramid": testPyramid,
3798
4283
  "ubiquitous-language": ubiquitousLanguage,
3799
4284
  "ux-audit": uxAudit
@@ -4022,7 +4507,7 @@ async function loadStoredToken(fileSystem, homeDir) {
4022
4507
  const data = JSON.parse(content);
4023
4508
  return typeof data.token === "string" ? data.token : null;
4024
4509
  } catch (error) {
4025
- if (error.code === "ENOENT") {
4510
+ if (isErrorCode(error, "ENOENT")) {
4026
4511
  return null;
4027
4512
  }
4028
4513
  throw error;
@@ -4038,7 +4523,7 @@ async function clearToken(fileSystem, homeDir) {
4038
4523
  try {
4039
4524
  await fileSystem.writeFile(path, "{}");
4040
4525
  } catch (error) {
4041
- if (error.code === "ENOENT") {
4526
+ if (isErrorCode(error, "ENOENT")) {
4042
4527
  return;
4043
4528
  }
4044
4529
  throw error;
@@ -5934,7 +6419,10 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
5934
6419
 
5935
6420
  // lib/loop/iteration.ts
5936
6421
  var log2 = createLogger("dust:loop:iteration");
5937
- var DUST_QUICK_REFERENCE = `## Dust Quick Reference
6422
+ function DUST_QUICK_REFERENCE(dustCommand) {
6423
+ return `## Dust Quick Reference
6424
+
6425
+ Dust is a CLI tool for managing development workflows through markdown artifacts. In this environment, run dust commands using: \`${dustCommand}\` (this might be \`dust\`, \`bunx dust\`, \`npx dust\`, or another prefix depending on how dust is installed).
5938
6426
 
5939
6427
  Dust stores project context in \`.dust/\` as markdown artifacts. Use these commands to explore:
5940
6428
 
@@ -5944,6 +6432,7 @@ Dust stores project context in \`.dust/\` as markdown artifacts. Use these comma
5944
6432
  - \`dust help\` — see all available commands
5945
6433
 
5946
6434
  Use dust commands instead of manually searching \`.dust/\` directories.`;
6435
+ }
5947
6436
  function getEnvironmentContext(cwd) {
5948
6437
  return {
5949
6438
  machineName: os.hostname(),
@@ -6158,7 +6647,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
6158
6647
  }
6159
6648
  const taskContent = await fileSystem.readFile(`${context.cwd}/${task.path}`);
6160
6649
  const instructions = buildImplementationInstructions(settings.dustCommand, hooksInstalled, task.title ?? undefined, task.path, settings.installCommand, true);
6161
- const taskPrompt = buildTaskPrompt(task.path, taskContent, instructions, toolsSection, options.branch);
6650
+ const taskPrompt = buildTaskPrompt(task.path, taskContent, instructions, toolsSection, settings.dustCommand, options.branch);
6162
6651
  let originalRemoteUrl;
6163
6652
  if (docker?.gitProxyUrl) {
6164
6653
  try {
@@ -6220,7 +6709,7 @@ Please resolve this issue. Common approaches:
6220
6709
 
6221
6710
  Make sure the repository is in a clean state and synced with remote before finishing.`;
6222
6711
  }
6223
- function buildTaskPrompt(taskPath, taskContent, instructions, toolsSection, branch) {
6712
+ function buildTaskPrompt(taskPath, taskContent, instructions, toolsSection, dustCommand, branch) {
6224
6713
  const suffix = toolsSection ? `
6225
6714
  ${toolsSection}` : "";
6226
6715
  const branchContext = branch ? `You are working on the \`${branch}\` branch.
@@ -6232,7 +6721,7 @@ ${toolsSection}` : "";
6232
6721
  ${taskContent}
6233
6722
  ----------
6234
6723
 
6235
- ${DUST_QUICK_REFERENCE}
6724
+ ${DUST_QUICK_REFERENCE(dustCommand)}
6236
6725
 
6237
6726
  ## How to implement the task
6238
6727
 
@@ -10097,7 +10586,7 @@ async function validateContentDirectoryFiles(dirPath, fileSystem) {
10097
10586
  try {
10098
10587
  entries = await fileSystem.readdir(dirPath);
10099
10588
  } catch (error) {
10100
- if (error.code === "ENOENT") {
10589
+ if (isErrorCode(error, "ENOENT")) {
10101
10590
  return [];
10102
10591
  }
10103
10592
  throw error;
@@ -10133,7 +10622,7 @@ async function validateDirectoryStructure(dustPath, fileSystem) {
10133
10622
  try {
10134
10623
  entries = await fileSystem.readdir(dustPath);
10135
10624
  } catch (error) {
10136
- if (error.code === "ENOENT") {
10625
+ if (isErrorCode(error, "ENOENT")) {
10137
10626
  return [];
10138
10627
  }
10139
10628
  throw error;
@@ -10179,7 +10668,7 @@ async function validateDirectoryStructure(dustPath, fileSystem) {
10179
10668
  try {
10180
10669
  configEntries = await fileSystem.readdir(configPath);
10181
10670
  } catch (error) {
10182
- if (error.code === "ENOENT") {
10671
+ if (isErrorCode(error, "ENOENT")) {
10183
10672
  return violations;
10184
10673
  }
10185
10674
  throw error;
@@ -10617,7 +11106,7 @@ async function parseArtifacts(fileSystem, dustPath) {
10617
11106
  try {
10618
11107
  rootEntries = await fileSystem.readdir(dustPath);
10619
11108
  } catch (error) {
10620
- if (error.code === "ENOENT") {
11109
+ if (isErrorCode(error, "ENOENT")) {
10621
11110
  rootEntries = [];
10622
11111
  } else {
10623
11112
  throw error;
@@ -10631,7 +11120,7 @@ async function parseArtifacts(fileSystem, dustPath) {
10631
11120
  try {
10632
11121
  content = await fileSystem.readFile(filePath);
10633
11122
  } catch (error) {
10634
- if (error.code === "ENOENT") {
11123
+ if (isErrorCode(error, "ENOENT")) {
10635
11124
  continue;
10636
11125
  }
10637
11126
  throw error;
@@ -10647,7 +11136,7 @@ async function parseArtifacts(fileSystem, dustPath) {
10647
11136
  try {
10648
11137
  entries = await fileSystem.readdir(dirPath);
10649
11138
  } catch (error) {
10650
- if (error.code === "ENOENT") {
11139
+ if (isErrorCode(error, "ENOENT")) {
10651
11140
  continue;
10652
11141
  }
10653
11142
  throw error;
@@ -10675,7 +11164,7 @@ async function parseArtifacts(fileSystem, dustPath) {
10675
11164
  try {
10676
11165
  auditEntries = await fileSystem.readdir(auditsPath);
10677
11166
  } catch (error) {
10678
- if (error.code === "ENOENT") {
11167
+ if (isErrorCode(error, "ENOENT")) {
10679
11168
  auditEntries = [];
10680
11169
  } else {
10681
11170
  throw error;
@@ -10689,7 +11178,7 @@ async function parseArtifacts(fileSystem, dustPath) {
10689
11178
  try {
10690
11179
  content = await fileSystem.readFile(filePath);
10691
11180
  } catch (error) {
10692
- if (error.code === "ENOENT") {
11181
+ if (isErrorCode(error, "ENOENT")) {
10693
11182
  continue;
10694
11183
  }
10695
11184
  throw error;
@@ -10774,7 +11263,7 @@ async function safeReadFile(fileSystem, filePath) {
10774
11263
  try {
10775
11264
  return await fileSystem.readFile(filePath);
10776
11265
  } catch (error) {
10777
- if (error.code === "ENOENT") {
11266
+ if (isErrorCode(error, "ENOENT")) {
10778
11267
  return null;
10779
11268
  }
10780
11269
  throw error;
@@ -12662,7 +13151,7 @@ async function init(dependencies) {
12662
13151
  await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT, { flag: "wx" });
12663
13152
  dustDirCreated = true;
12664
13153
  } catch (error) {
12665
- if (error.code !== "EEXIST") {
13154
+ if (!isErrorCode(error, "EEXIST")) {
12666
13155
  throw error;
12667
13156
  }
12668
13157
  }
@@ -12671,7 +13160,7 @@ async function init(dependencies) {
12671
13160
  await fileSystem.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
12672
13161
  `, { flag: "wx" });
12673
13162
  } catch (error) {
12674
- if (error.code !== "EEXIST") {
13163
+ if (!isErrorCode(error, "EEXIST")) {
12675
13164
  throw error;
12676
13165
  }
12677
13166
  }
@@ -12690,7 +13179,7 @@ async function init(dependencies) {
12690
13179
  });
12691
13180
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
12692
13181
  } catch (error) {
12693
- if (error.code === "EEXIST") {
13182
+ if (isErrorCode(error, "EEXIST")) {
12694
13183
  context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
12695
13184
  } else {
12696
13185
  throw error;
@@ -12703,7 +13192,7 @@ async function init(dependencies) {
12703
13192
  });
12704
13193
  context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
12705
13194
  } catch (error) {
12706
- if (error.code === "EEXIST") {
13195
+ if (isErrorCode(error, "EEXIST")) {
12707
13196
  context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
12708
13197
  } else {
12709
13198
  throw error;
@@ -13331,7 +13820,7 @@ async function scanMarkdownFiles(glob, dirPath) {
13331
13820
  }
13332
13821
  return files;
13333
13822
  } catch (error) {
13334
- if (error.code === "ENOENT") {
13823
+ if (isErrorCode(error, "ENOENT")) {
13335
13824
  return [];
13336
13825
  }
13337
13826
  throw error;