@joshski/dust 0.1.57 → 0.1.59

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,7 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
2
5
  // lib/cli/run.ts
3
6
  import { existsSync, statSync as statSync2 } from "node:fs";
4
- import { chmod as chmod2, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
7
+ import {
8
+ chmod as chmod2,
9
+ mkdir as mkdir2,
10
+ readdir as readdir2,
11
+ readFile as readFile2,
12
+ rename,
13
+ writeFile as writeFile2
14
+ } from "node:fs/promises";
15
+
16
+ // lib/git/file-sorter.ts
17
+ function createGitDirectoryFileSorter(gitRunner) {
18
+ return async (dir, files) => {
19
+ const timestamps = await Promise.all(files.map(async (file) => {
20
+ const result = await gitRunner.run(["log", "-1", "--format=%ct", "--", file], dir);
21
+ const ts = result.exitCode === 0 ? Number.parseInt(result.output.trim(), 10) : Number.NaN;
22
+ return {
23
+ file,
24
+ timestamp: Number.isNaN(ts) ? Number.POSITIVE_INFINITY : ts
25
+ };
26
+ }));
27
+ timestamps.sort((a, b) => a.timestamp - b.timestamp);
28
+ return timestamps.map((t) => t.file);
29
+ };
30
+ }
5
31
 
6
32
  // lib/config/settings.ts
7
33
  import { join } from "node:path";
@@ -481,8 +507,8 @@ ${vars.agentInstructions}` : "";
481
507
  3. **Capture a new task** → \`${vars.bin} new task\`
482
508
  User has concrete work to add. Keywords: "task: ..." or "add a task ..."
483
509
 
484
- 4. **Capture a new goal** → \`${vars.bin} new goal\`
485
- User has a higher-level objective to add. Keywords: "goal: ..." or "add a goal ..."
510
+ 4. **Capture a new principle** → \`${vars.bin} new principle\`
511
+ User has a guiding value to add. Keywords: "principle: ..." or "add a principle ..."
486
512
 
487
513
  5. **Capture a vague idea** → \`${vars.bin} new idea\`
488
514
  User has a rough idea that might become work later. Keywords: "idea: ..." or "add an idea ..."
@@ -577,7 +603,7 @@ function agentDeveloperExperience() {
577
603
  4. **Debugging tools** - Can agents diagnose issues without trial and error?
578
604
  5. **Structured logging** - Is system behavior observable through logs?
579
605
 
580
- ## Goals
606
+ ## Principles
581
607
 
582
608
  (none)
583
609
 
@@ -610,7 +636,7 @@ function deadCode() {
610
636
  4. **Unused dependencies** - Packages in package.json not used in code
611
637
  5. **Commented-out code** - Old code left in comments
612
638
 
613
- ## Goals
639
+ ## Principles
614
640
 
615
641
  (none)
616
642
 
@@ -644,7 +670,7 @@ function factsVerification() {
644
670
  3. **Staleness** - Have facts become outdated due to recent changes?
645
671
  4. **Relevance** - Are all facts still useful for understanding the project?
646
672
 
647
- ## Goals
673
+ ## Principles
648
674
 
649
675
  (none)
650
676
 
@@ -677,7 +703,7 @@ function ideasFromCommits() {
677
703
  3. **Pattern opportunities** - Can recent changes be generalized?
678
704
  4. **Test gaps** - Do recent changes have adequate test coverage?
679
705
 
680
- ## Goals
706
+ ## Principles
681
707
 
682
708
  (none)
683
709
 
@@ -694,22 +720,22 @@ function ideasFromCommits() {
694
720
  - [ ] Proposed follow-up ideas for any issues identified
695
721
  `;
696
722
  }
697
- function ideasFromGoals() {
723
+ function ideasFromPrinciples() {
698
724
  return dedent`
699
- # Ideas from Goals
725
+ # Ideas from Principles
700
726
 
701
- 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.
727
+ Review \`.dust/principles/\` to generate new improvement ideas. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues or opportunities you identify, avoiding duplication.
702
728
 
703
729
  ## Scope
704
730
 
705
731
  Focus on these areas:
706
732
 
707
- 1. **Unmet goals** - Which goals lack supporting work?
708
- 2. **Gap analysis** - Where does the codebase fall short of goals?
709
- 3. **New opportunities** - What work would better achieve each goal?
710
- 4. **Goal alignment** - Are current tasks aligned with stated goals?
733
+ 1. **Unmet principles** - Which principles lack supporting work?
734
+ 2. **Gap analysis** - Where does the codebase fall short of principles?
735
+ 3. **New opportunities** - What work would better achieve each principle?
736
+ 4. **Principle alignment** - Are current tasks aligned with stated principles?
711
737
 
712
- ## Goals
738
+ ## Principles
713
739
 
714
740
  (none)
715
741
 
@@ -719,10 +745,10 @@ function ideasFromGoals() {
719
745
 
720
746
  ## Definition of Done
721
747
 
722
- - [ ] Read each goal file in \`.dust/goals/\`
723
- - [ ] Analyzed codebase for alignment with each goal
724
- - [ ] Listed gaps between current state and goal intent
725
- - [ ] Proposed new ideas for unmet or underserved goals
748
+ - [ ] Read each principle file in \`.dust/principles/\`
749
+ - [ ] Analyzed codebase for alignment with each principle
750
+ - [ ] Listed gaps between current state and principle intent
751
+ - [ ] Proposed new ideas for unmet or underserved principles
726
752
  `;
727
753
  }
728
754
  function performanceReview() {
@@ -741,7 +767,7 @@ function performanceReview() {
741
767
  4. **Build performance** - How fast is the build process?
742
768
  5. **Test speed** - Are tests running efficiently?
743
769
 
744
- ## Goals
770
+ ## Principles
745
771
 
746
772
  (none)
747
773
 
@@ -774,7 +800,7 @@ function securityReview() {
774
800
  4. **Sensitive data exposure** - Logging sensitive data, insecure storage
775
801
  5. **Dependency vulnerabilities** - Known CVEs in dependencies
776
802
 
777
- ## Goals
803
+ ## Principles
778
804
 
779
805
  (none)
780
806
 
@@ -808,7 +834,7 @@ function staleIdeas() {
808
834
  3. **Actionability** - Can the idea be converted to a task?
809
835
  4. **Duplication** - Are there overlapping or redundant ideas?
810
836
 
811
- ## Goals
837
+ ## Principles
812
838
 
813
839
  (none)
814
840
 
@@ -841,7 +867,7 @@ function testCoverage() {
841
867
  4. **User-facing features** - UI components, form validation
842
868
  5. **Recent changes** - Code modified in the last few commits
843
869
 
844
- ## Goals
870
+ ## Principles
845
871
 
846
872
  (none)
847
873
 
@@ -862,7 +888,7 @@ var stockAuditFunctions = {
862
888
  "dead-code": deadCode,
863
889
  "facts-verification": factsVerification,
864
890
  "ideas-from-commits": ideasFromCommits,
865
- "ideas-from-goals": ideasFromGoals,
891
+ "ideas-from-principles": ideasFromPrinciples,
866
892
  "performance-review": performanceReview,
867
893
  "security-review": securityReview,
868
894
  "stale-ideas": staleIdeas,
@@ -1657,39 +1683,68 @@ class FileSink {
1657
1683
 
1658
1684
  // lib/logging/index.ts
1659
1685
  var DUST_LOG_FILE = "DUST_LOG_FILE";
1660
- var patterns = null;
1661
- var initialized = false;
1662
- var activeFileSink = null;
1663
- var ownedDustLogFile = false;
1664
- function init() {
1665
- if (initialized)
1666
- return;
1667
- initialized = true;
1668
- const parsed = parsePatterns(process.env.DEBUG);
1669
- patterns = parsed.length > 0 ? parsed : null;
1670
- }
1671
- function enableFileLogs(scope, _sinkForTesting) {
1672
- const existing = process.env[DUST_LOG_FILE];
1673
- const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
1674
- const path = existing ?? join5(logDir, `${scope}.log`);
1675
- if (!existing) {
1676
- process.env[DUST_LOG_FILE] = path;
1677
- ownedDustLogFile = true;
1678
- }
1679
- activeFileSink = _sinkForTesting ?? new FileSink(path);
1680
- }
1681
- function createLogger(name) {
1682
- return (...messages) => {
1683
- init();
1684
- const line = formatLine(name, messages);
1685
- if (activeFileSink) {
1686
- activeFileSink.write(line);
1687
- }
1688
- if (patterns && matchesAny(name, patterns)) {
1689
- process.stdout.write(line);
1686
+ function createLoggingService() {
1687
+ let patterns = null;
1688
+ let initialized = false;
1689
+ let activeFileSink = null;
1690
+ const fileSinkCache = new Map;
1691
+ function init() {
1692
+ if (initialized)
1693
+ return;
1694
+ initialized = true;
1695
+ const parsed = parsePatterns(process.env.DEBUG);
1696
+ patterns = parsed.length > 0 ? parsed : null;
1697
+ }
1698
+ function getOrCreateFileSink(path) {
1699
+ let sink = fileSinkCache.get(path);
1700
+ if (!sink) {
1701
+ sink = new FileSink(path);
1702
+ fileSinkCache.set(path, sink);
1703
+ }
1704
+ return sink;
1705
+ }
1706
+ return {
1707
+ enableFileLogs(scope, sinkForTesting) {
1708
+ const existing = process.env[DUST_LOG_FILE];
1709
+ const logDir = process.env.DUST_LOG_DIR ?? join5(process.cwd(), "log");
1710
+ const path = existing ?? join5(logDir, `${scope}.log`);
1711
+ if (!existing) {
1712
+ process.env[DUST_LOG_FILE] = path;
1713
+ }
1714
+ activeFileSink = sinkForTesting ?? new FileSink(path);
1715
+ },
1716
+ createLogger(name, options) {
1717
+ let perLoggerSink;
1718
+ if (options?.file === false) {
1719
+ perLoggerSink = null;
1720
+ } else if (typeof options?.file === "string") {
1721
+ perLoggerSink = getOrCreateFileSink(options.file);
1722
+ }
1723
+ return (...messages) => {
1724
+ init();
1725
+ const line = formatLine(name, messages);
1726
+ if (perLoggerSink !== undefined) {
1727
+ if (perLoggerSink !== null) {
1728
+ perLoggerSink.write(line);
1729
+ }
1730
+ } else if (activeFileSink) {
1731
+ activeFileSink.write(line);
1732
+ }
1733
+ if (patterns && matchesAny(name, patterns)) {
1734
+ process.stdout.write(line);
1735
+ }
1736
+ };
1737
+ },
1738
+ isEnabled(name) {
1739
+ init();
1740
+ return patterns !== null && matchesAny(name, patterns);
1690
1741
  }
1691
1742
  };
1692
1743
  }
1744
+ var defaultService = createLoggingService();
1745
+ var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
1746
+ var createLogger = defaultService.createLogger.bind(defaultService);
1747
+ var isEnabled = defaultService.isEnabled.bind(defaultService);
1693
1748
 
1694
1749
  // lib/bucket/repository-git.ts
1695
1750
  import { join as join6 } from "node:path";
@@ -1845,7 +1900,7 @@ function extractBlockedBy(content) {
1845
1900
  }
1846
1901
  return blockers;
1847
1902
  }
1848
- async function findUnblockedTasks(cwd, fileSystem) {
1903
+ async function findUnblockedTasks(cwd, fileSystem, directoryFileSorter) {
1849
1904
  const dustPath = `${cwd}/.dust`;
1850
1905
  if (!fileSystem.exists(dustPath)) {
1851
1906
  return { error: ".dust directory not found", tasks: [] };
@@ -1855,11 +1910,16 @@ async function findUnblockedTasks(cwd, fileSystem) {
1855
1910
  return { tasks: [] };
1856
1911
  }
1857
1912
  const files = await fileSystem.readdir(tasksPath);
1858
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort((a, b) => {
1859
- const aTime = fileSystem.getFileCreationTime(`${tasksPath}/${a}`);
1860
- const bTime = fileSystem.getFileCreationTime(`${tasksPath}/${b}`);
1861
- return aTime - bTime;
1862
- });
1913
+ let mdFiles = files.filter((f) => f.endsWith(".md"));
1914
+ if (directoryFileSorter) {
1915
+ mdFiles = await directoryFileSorter(tasksPath, mdFiles);
1916
+ } else {
1917
+ mdFiles.sort((a, b) => {
1918
+ const aTime = fileSystem.getFileCreationTime(`${tasksPath}/${a}`);
1919
+ const bTime = fileSystem.getFileCreationTime(`${tasksPath}/${b}`);
1920
+ return aTime - bTime;
1921
+ });
1922
+ }
1863
1923
  if (mdFiles.length === 0) {
1864
1924
  return { tasks: [] };
1865
1925
  }
@@ -1895,8 +1955,8 @@ function printTaskList(context, tasks) {
1895
1955
  }
1896
1956
  }
1897
1957
  async function next(dependencies) {
1898
- const { context, fileSystem } = dependencies;
1899
- const result = await findUnblockedTasks(context.cwd, fileSystem);
1958
+ const { context, fileSystem, directoryFileSorter } = dependencies;
1959
+ const result = await findUnblockedTasks(context.cwd, fileSystem, directoryFileSorter);
1900
1960
  if (result.error) {
1901
1961
  context.stderr(`Error: ${result.error}`);
1902
1962
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -1993,7 +2053,7 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
1993
2053
  postEvent(eventsUrl, payload).catch(onError);
1994
2054
  };
1995
2055
  }
1996
- var log = createLogger("dust.cli.commands.loop");
2056
+ var log = createLogger("dust:cli:commands:loop");
1997
2057
  var SLEEP_INTERVAL_MS = 30000;
1998
2058
  var SLEEP_STEP_MS = 1000;
1999
2059
  var DEFAULT_MAX_ITERATIONS = 10;
@@ -2030,8 +2090,8 @@ async function gitPull(cwd, spawn) {
2030
2090
  });
2031
2091
  }
2032
2092
  async function findAvailableTasks(dependencies) {
2033
- const { context, fileSystem } = dependencies;
2034
- const result = await findUnblockedTasks(context.cwd, fileSystem);
2093
+ const { context, fileSystem, directoryFileSorter } = dependencies;
2094
+ const result = await findUnblockedTasks(context.cwd, fileSystem, directoryFileSorter);
2035
2095
  return result.tasks;
2036
2096
  }
2037
2097
  async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, options = {}) {
@@ -2224,7 +2284,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2224
2284
  }
2225
2285
 
2226
2286
  // lib/bucket/repository-loop.ts
2227
- var log2 = createLogger("dust.bucket.repository-loop");
2287
+ var log2 = createLogger("dust:bucket:repository-loop");
2228
2288
  var FALLBACK_TIMEOUT_MS = 300000;
2229
2289
  function createNoOpGlobScanner() {
2230
2290
  return {
@@ -2373,7 +2433,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2373
2433
  }
2374
2434
 
2375
2435
  // lib/bucket/repository.ts
2376
- var log3 = createLogger("dust.bucket.repository");
2436
+ var log3 = createLogger("dust:bucket:repository");
2377
2437
  function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2378
2438
  log3(`starting loop for ${repoState.repository.name}`);
2379
2439
  repoState.stopRequested = false;
@@ -2908,7 +2968,7 @@ function handleKeyInput(state, key, options) {
2908
2968
  }
2909
2969
 
2910
2970
  // lib/cli/commands/bucket.ts
2911
- var log4 = createLogger("dust.cli.commands.bucket");
2971
+ var log4 = createLogger("dust:cli:commands:bucket");
2912
2972
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
2913
2973
  var INITIAL_RECONNECT_DELAY_MS = 1000;
2914
2974
  var MAX_RECONNECT_DELAY_MS = 30000;
@@ -3018,7 +3078,8 @@ function createDefaultBucketDependencies() {
3018
3078
  writeFile: (path, content) => writeFile(path, content, "utf8"),
3019
3079
  mkdir: (path, options) => mkdir(path, options).then(() => {}),
3020
3080
  readdir: (path) => readdir(path),
3021
- chmod: (path, mode) => chmod(path, mode)
3081
+ chmod: (path, mode) => chmod(path, mode),
3082
+ rename: (oldPath, newPath) => import("node:fs/promises").then((mod) => mod.rename(oldPath, newPath))
3022
3083
  };
3023
3084
  return {
3024
3085
  spawn: nodeSpawn3,
@@ -3459,7 +3520,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3459
3520
  import { join as join10 } from "node:path";
3460
3521
 
3461
3522
  // lib/lint/validators/content-validator.ts
3462
- var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
3523
+ var REQUIRED_HEADINGS = [
3524
+ "## Principles",
3525
+ "## Blocked By",
3526
+ "## Definition of Done"
3527
+ ];
3463
3528
  var MAX_OPENING_SENTENCE_LENGTH = 150;
3464
3529
  var NON_IMPERATIVE_STARTERS = new Set([
3465
3530
  "the",
@@ -3528,7 +3593,7 @@ function validateTaskHeadings(filePath, content) {
3528
3593
  }
3529
3594
 
3530
3595
  // lib/lint/validators/directory-validator.ts
3531
- var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
3596
+ var EXPECTED_DIRECTORIES = ["principles", "ideas", "tasks", "facts", "config"];
3532
3597
  async function validateContentDirectoryFiles(dirPath, fileSystem) {
3533
3598
  const violations = [];
3534
3599
  let entries;
@@ -3626,117 +3691,6 @@ function validateTitleFilenameMatch(filePath, content) {
3626
3691
  return null;
3627
3692
  }
3628
3693
 
3629
- // lib/lint/validators/goal-hierarchy.ts
3630
- import { dirname as dirname4, resolve } from "node:path";
3631
- var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
3632
- function validateGoalHierarchySections(filePath, content) {
3633
- const violations = [];
3634
- for (const heading of REQUIRED_GOAL_HEADINGS) {
3635
- if (!content.includes(heading)) {
3636
- violations.push({
3637
- file: filePath,
3638
- message: `Missing required heading: "${heading}"`
3639
- });
3640
- }
3641
- }
3642
- return violations;
3643
- }
3644
- function extractGoalRelationships(filePath, content) {
3645
- const lines = content.split(`
3646
- `);
3647
- const fileDir = dirname4(filePath);
3648
- const parentGoals = [];
3649
- const subGoals = [];
3650
- let currentSection = null;
3651
- for (const line of lines) {
3652
- if (line.startsWith("## ")) {
3653
- currentSection = line;
3654
- continue;
3655
- }
3656
- if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
3657
- continue;
3658
- }
3659
- const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
3660
- let match = linkPattern.exec(line);
3661
- while (match) {
3662
- const linkTarget = match[2];
3663
- if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
3664
- const targetPath = linkTarget.split("#")[0];
3665
- const resolvedPath = resolve(fileDir, targetPath);
3666
- if (resolvedPath.includes("/.dust/goals/")) {
3667
- if (currentSection === "## Parent Goal") {
3668
- parentGoals.push(resolvedPath);
3669
- } else {
3670
- subGoals.push(resolvedPath);
3671
- }
3672
- }
3673
- }
3674
- match = linkPattern.exec(line);
3675
- }
3676
- }
3677
- return { filePath, parentGoals, subGoals };
3678
- }
3679
- function validateBidirectionalLinks(allGoalRelationships) {
3680
- const violations = [];
3681
- const relationshipMap = new Map;
3682
- for (const rel of allGoalRelationships) {
3683
- relationshipMap.set(rel.filePath, rel);
3684
- }
3685
- for (const rel of allGoalRelationships) {
3686
- for (const parentPath of rel.parentGoals) {
3687
- const parentRel = relationshipMap.get(parentPath);
3688
- if (parentRel && !parentRel.subGoals.includes(rel.filePath)) {
3689
- violations.push({
3690
- file: rel.filePath,
3691
- message: `Parent goal "${parentPath}" does not list this goal as a sub-goal`
3692
- });
3693
- }
3694
- }
3695
- for (const subGoalPath of rel.subGoals) {
3696
- const subGoalRel = relationshipMap.get(subGoalPath);
3697
- if (subGoalRel && !subGoalRel.parentGoals.includes(rel.filePath)) {
3698
- violations.push({
3699
- file: rel.filePath,
3700
- message: `Sub-goal "${subGoalPath}" does not list this goal as its parent`
3701
- });
3702
- }
3703
- }
3704
- }
3705
- return violations;
3706
- }
3707
- function validateNoCycles(allGoalRelationships) {
3708
- const violations = [];
3709
- const relationshipMap = new Map;
3710
- for (const rel of allGoalRelationships) {
3711
- relationshipMap.set(rel.filePath, rel);
3712
- }
3713
- for (const rel of allGoalRelationships) {
3714
- const visited = new Set;
3715
- const path = [];
3716
- let current = rel.filePath;
3717
- while (current) {
3718
- if (visited.has(current)) {
3719
- const cycleStart = path.indexOf(current);
3720
- const cyclePath = path.slice(cycleStart).concat(current);
3721
- violations.push({
3722
- file: rel.filePath,
3723
- message: `Cycle detected in goal hierarchy: ${cyclePath.join(" -> ")}`
3724
- });
3725
- break;
3726
- }
3727
- visited.add(current);
3728
- path.push(current);
3729
- const currentRel = relationshipMap.get(current);
3730
- if (currentRel && currentRel.parentGoals.length > 0) {
3731
- current = currentRel.parentGoals[0];
3732
- } else {
3733
- current = null;
3734
- }
3735
- }
3736
- }
3737
- return violations;
3738
- }
3739
-
3740
3694
  // lib/lint/validators/idea-validator.ts
3741
3695
  function validateIdeaOpenQuestions(filePath, content) {
3742
3696
  const violations = [];
@@ -3853,12 +3807,12 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
3853
3807
  }
3854
3808
 
3855
3809
  // lib/lint/validators/link-validator.ts
3856
- import { dirname as dirname5, resolve as resolve2 } from "node:path";
3810
+ import { dirname as dirname4, resolve } from "node:path";
3857
3811
  var SEMANTIC_RULES = [
3858
3812
  {
3859
- section: "## Goals",
3860
- requiredPath: "/.dust/goals/",
3861
- description: "goal"
3813
+ section: "## Principles",
3814
+ requiredPath: "/.dust/principles/",
3815
+ description: "principle"
3862
3816
  },
3863
3817
  {
3864
3818
  section: "## Blocked By",
@@ -3870,7 +3824,7 @@ function validateLinks(filePath, content, fileSystem) {
3870
3824
  const violations = [];
3871
3825
  const lines = content.split(`
3872
3826
  `);
3873
- const fileDir = dirname5(filePath);
3827
+ const fileDir = dirname4(filePath);
3874
3828
  for (let i = 0;i < lines.length; i++) {
3875
3829
  const line = lines[i];
3876
3830
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3879,7 +3833,7 @@ function validateLinks(filePath, content, fileSystem) {
3879
3833
  const linkTarget = match[2];
3880
3834
  if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
3881
3835
  const targetPath = linkTarget.split("#")[0];
3882
- const resolvedPath = resolve2(fileDir, targetPath);
3836
+ const resolvedPath = resolve(fileDir, targetPath);
3883
3837
  if (!fileSystem.exists(resolvedPath)) {
3884
3838
  violations.push({
3885
3839
  file: filePath,
@@ -3897,7 +3851,7 @@ function validateSemanticLinks(filePath, content) {
3897
3851
  const violations = [];
3898
3852
  const lines = content.split(`
3899
3853
  `);
3900
- const fileDir = dirname5(filePath);
3854
+ const fileDir = dirname4(filePath);
3901
3855
  let currentSection = null;
3902
3856
  for (let i = 0;i < lines.length; i++) {
3903
3857
  const line = lines[i];
@@ -3931,7 +3885,7 @@ function validateSemanticLinks(filePath, content) {
3931
3885
  continue;
3932
3886
  }
3933
3887
  const targetPath = linkTarget.split("#")[0];
3934
- const resolvedPath = resolve2(fileDir, targetPath);
3888
+ const resolvedPath = resolve(fileDir, targetPath);
3935
3889
  if (!resolvedPath.includes(rule.requiredPath)) {
3936
3890
  violations.push({
3937
3891
  file: filePath,
@@ -3944,11 +3898,11 @@ function validateSemanticLinks(filePath, content) {
3944
3898
  }
3945
3899
  return violations;
3946
3900
  }
3947
- function validateGoalHierarchyLinks(filePath, content) {
3901
+ function validatePrincipleHierarchyLinks(filePath, content) {
3948
3902
  const violations = [];
3949
3903
  const lines = content.split(`
3950
3904
  `);
3951
- const fileDir = dirname5(filePath);
3905
+ const fileDir = dirname4(filePath);
3952
3906
  let currentSection = null;
3953
3907
  for (let i = 0;i < lines.length; i++) {
3954
3908
  const line = lines[i];
@@ -3956,7 +3910,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3956
3910
  currentSection = line;
3957
3911
  continue;
3958
3912
  }
3959
- if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
3913
+ if (currentSection !== "## Parent Principle" && currentSection !== "## Sub-Principles") {
3960
3914
  continue;
3961
3915
  }
3962
3916
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3966,7 +3920,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3966
3920
  if (linkTarget.startsWith("#")) {
3967
3921
  violations.push({
3968
3922
  file: filePath,
3969
- message: `Link in "${currentSection}" must point to a goal file, not an anchor: "${linkTarget}"`,
3923
+ message: `Link in "${currentSection}" must point to a principle file, not an anchor: "${linkTarget}"`,
3970
3924
  line: i + 1
3971
3925
  });
3972
3926
  match = linkPattern.exec(line);
@@ -3975,18 +3929,18 @@ function validateGoalHierarchyLinks(filePath, content) {
3975
3929
  if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
3976
3930
  violations.push({
3977
3931
  file: filePath,
3978
- message: `Link in "${currentSection}" must point to a goal file, not an external URL: "${linkTarget}"`,
3932
+ message: `Link in "${currentSection}" must point to a principle file, not an external URL: "${linkTarget}"`,
3979
3933
  line: i + 1
3980
3934
  });
3981
3935
  match = linkPattern.exec(line);
3982
3936
  continue;
3983
3937
  }
3984
3938
  const targetPath = linkTarget.split("#")[0];
3985
- const resolvedPath = resolve2(fileDir, targetPath);
3986
- if (!resolvedPath.includes("/.dust/goals/")) {
3939
+ const resolvedPath = resolve(fileDir, targetPath);
3940
+ if (!resolvedPath.includes("/.dust/principles/")) {
3987
3941
  violations.push({
3988
3942
  file: filePath,
3989
- message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
3943
+ message: `Link in "${currentSection}" must point to a principle file: "${linkTarget}"`,
3990
3944
  line: i + 1
3991
3945
  });
3992
3946
  }
@@ -3996,6 +3950,117 @@ function validateGoalHierarchyLinks(filePath, content) {
3996
3950
  return violations;
3997
3951
  }
3998
3952
 
3953
+ // lib/lint/validators/principle-hierarchy.ts
3954
+ import { dirname as dirname5, resolve as resolve2 } from "node:path";
3955
+ var REQUIRED_PRINCIPLE_HEADINGS = ["## Parent Principle", "## Sub-Principles"];
3956
+ function validatePrincipleHierarchySections(filePath, content) {
3957
+ const violations = [];
3958
+ for (const heading of REQUIRED_PRINCIPLE_HEADINGS) {
3959
+ if (!content.includes(heading)) {
3960
+ violations.push({
3961
+ file: filePath,
3962
+ message: `Missing required heading: "${heading}"`
3963
+ });
3964
+ }
3965
+ }
3966
+ return violations;
3967
+ }
3968
+ function extractPrincipleRelationships(filePath, content) {
3969
+ const lines = content.split(`
3970
+ `);
3971
+ const fileDir = dirname5(filePath);
3972
+ const parentPrinciples = [];
3973
+ const subPrinciples = [];
3974
+ let currentSection = null;
3975
+ for (const line of lines) {
3976
+ if (line.startsWith("## ")) {
3977
+ currentSection = line;
3978
+ continue;
3979
+ }
3980
+ if (currentSection !== "## Parent Principle" && currentSection !== "## Sub-Principles") {
3981
+ continue;
3982
+ }
3983
+ const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
3984
+ let match = linkPattern.exec(line);
3985
+ while (match) {
3986
+ const linkTarget = match[2];
3987
+ if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
3988
+ const targetPath = linkTarget.split("#")[0];
3989
+ const resolvedPath = resolve2(fileDir, targetPath);
3990
+ if (resolvedPath.includes("/.dust/principles/")) {
3991
+ if (currentSection === "## Parent Principle") {
3992
+ parentPrinciples.push(resolvedPath);
3993
+ } else {
3994
+ subPrinciples.push(resolvedPath);
3995
+ }
3996
+ }
3997
+ }
3998
+ match = linkPattern.exec(line);
3999
+ }
4000
+ }
4001
+ return { filePath, parentPrinciples, subPrinciples };
4002
+ }
4003
+ function validateBidirectionalLinks(allPrincipleRelationships) {
4004
+ const violations = [];
4005
+ const relationshipMap = new Map;
4006
+ for (const rel of allPrincipleRelationships) {
4007
+ relationshipMap.set(rel.filePath, rel);
4008
+ }
4009
+ for (const rel of allPrincipleRelationships) {
4010
+ for (const parentPath of rel.parentPrinciples) {
4011
+ const parentRel = relationshipMap.get(parentPath);
4012
+ if (parentRel && !parentRel.subPrinciples.includes(rel.filePath)) {
4013
+ violations.push({
4014
+ file: rel.filePath,
4015
+ message: `Parent principle "${parentPath}" does not list this principle as a sub-principle`
4016
+ });
4017
+ }
4018
+ }
4019
+ for (const subPrinciplePath of rel.subPrinciples) {
4020
+ const subPrincipleRel = relationshipMap.get(subPrinciplePath);
4021
+ if (subPrincipleRel && !subPrincipleRel.parentPrinciples.includes(rel.filePath)) {
4022
+ violations.push({
4023
+ file: rel.filePath,
4024
+ message: `Sub-principle "${subPrinciplePath}" does not list this principle as its parent`
4025
+ });
4026
+ }
4027
+ }
4028
+ }
4029
+ return violations;
4030
+ }
4031
+ function validateNoCycles(allPrincipleRelationships) {
4032
+ const violations = [];
4033
+ const relationshipMap = new Map;
4034
+ for (const rel of allPrincipleRelationships) {
4035
+ relationshipMap.set(rel.filePath, rel);
4036
+ }
4037
+ for (const rel of allPrincipleRelationships) {
4038
+ const visited = new Set;
4039
+ const path = [];
4040
+ let current = rel.filePath;
4041
+ while (current) {
4042
+ if (visited.has(current)) {
4043
+ const cycleStart = path.indexOf(current);
4044
+ const cyclePath = path.slice(cycleStart).concat(current);
4045
+ violations.push({
4046
+ file: rel.filePath,
4047
+ message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
4048
+ });
4049
+ break;
4050
+ }
4051
+ visited.add(current);
4052
+ path.push(current);
4053
+ const currentRel = relationshipMap.get(current);
4054
+ if (currentRel && currentRel.parentPrinciples.length > 0) {
4055
+ current = currentRel.parentPrinciples[0];
4056
+ } else {
4057
+ current = null;
4058
+ }
4059
+ }
4060
+ }
4061
+ return violations;
4062
+ }
4063
+
3999
4064
  // lib/cli/commands/lint-markdown.ts
4000
4065
  async function safeScanDir(glob, dirPath) {
4001
4066
  const files = [];
@@ -4056,7 +4121,7 @@ async function lintMarkdown(dependencies) {
4056
4121
  }
4057
4122
  }
4058
4123
  }
4059
- const contentDirs = ["goals", "facts", "ideas", "tasks"];
4124
+ const contentDirs = ["principles", "facts", "ideas", "tasks"];
4060
4125
  context.stdout("Validating content files...");
4061
4126
  for (const dir of contentDirs) {
4062
4127
  const dirPath = `${dustPath}/${dir}`;
@@ -4145,15 +4210,15 @@ async function lintMarkdown(dependencies) {
4145
4210
  }
4146
4211
  }
4147
4212
  }
4148
- const goalsPath = `${dustPath}/goals`;
4149
- const { files: goalFiles } = await safeScanDir(glob, goalsPath);
4150
- if (goalFiles.length > 0) {
4151
- context.stdout("Validating goal hierarchy in .dust/goals/...");
4152
- const allGoalRelationships = [];
4153
- for (const file of goalFiles) {
4213
+ const principlesPath = `${dustPath}/principles`;
4214
+ const { files: principleFiles } = await safeScanDir(glob, principlesPath);
4215
+ if (principleFiles.length > 0) {
4216
+ context.stdout("Validating principle hierarchy in .dust/principles/...");
4217
+ const allPrincipleRelationships = [];
4218
+ for (const file of principleFiles) {
4154
4219
  if (!file.endsWith(".md"))
4155
4220
  continue;
4156
- const filePath = `${goalsPath}/${file}`;
4221
+ const filePath = `${principlesPath}/${file}`;
4157
4222
  let content;
4158
4223
  try {
4159
4224
  content = await fileSystem.readFile(filePath);
@@ -4163,12 +4228,12 @@ async function lintMarkdown(dependencies) {
4163
4228
  }
4164
4229
  throw error;
4165
4230
  }
4166
- violations.push(...validateGoalHierarchySections(filePath, content));
4167
- violations.push(...validateGoalHierarchyLinks(filePath, content));
4168
- allGoalRelationships.push(extractGoalRelationships(filePath, content));
4231
+ violations.push(...validatePrincipleHierarchySections(filePath, content));
4232
+ violations.push(...validatePrincipleHierarchyLinks(filePath, content));
4233
+ allPrincipleRelationships.push(extractPrincipleRelationships(filePath, content));
4169
4234
  }
4170
- violations.push(...validateBidirectionalLinks(allGoalRelationships));
4171
- violations.push(...validateNoCycles(allGoalRelationships));
4235
+ violations.push(...validateBidirectionalLinks(allPrincipleRelationships));
4236
+ violations.push(...validateNoCycles(allPrincipleRelationships));
4172
4237
  }
4173
4238
  if (violations.length === 0) {
4174
4239
  context.stdout("All validations passed!");
@@ -4185,7 +4250,7 @@ async function lintMarkdown(dependencies) {
4185
4250
  }
4186
4251
 
4187
4252
  // lib/cli/commands/check.ts
4188
- var log5 = createLogger("dust.cli.commands.check");
4253
+ var log5 = createLogger("dust:cli:commands:check");
4189
4254
  var DEFAULT_CHECK_TIMEOUT_MS = 13000;
4190
4255
  var MAX_OUTPUT_LINES = 500;
4191
4256
  var KEEP_LINES = 250;
@@ -4361,10 +4426,10 @@ function generateHelpText(settings) {
4361
4426
  Commands:
4362
4427
  init Initialize a new Dust repository
4363
4428
  lint Run lint checks on .dust/ files
4364
- list List all items (tasks, ideas, goals, facts)
4429
+ list List all items (tasks, ideas, principles, facts)
4365
4430
  tasks List tasks (actionable work with definitions of done)
4366
4431
  ideas List ideas (vague proposals, convert to tasks when ready)
4367
- goals List goals (guiding principles, stable, rarely change)
4432
+ principles List principles (guiding values, stable, rarely change)
4368
4433
  facts List facts (documentation of current system state)
4369
4434
  next Show tasks ready to work on (not blocked)
4370
4435
  check Run project-defined quality gate hook
@@ -4373,7 +4438,7 @@ function generateHelpText(settings) {
4373
4438
  pick task Pick the next task to work on
4374
4439
  implement task Implement a task
4375
4440
  new task Create a new task
4376
- new goal Create a new goal
4441
+ new principle Create a new principle
4377
4442
  new idea Create a new idea
4378
4443
  loop claude Run continuous Claude iteration on tasks
4379
4444
  pre push Git pre-push hook validation
@@ -4382,10 +4447,10 @@ function generateHelpText(settings) {
4382
4447
  🤖 Agent Guide
4383
4448
 
4384
4449
  Dust is a lightweight planning system. The .dust/ directory contains:
4385
- - goals/ - Guiding principles (stable, rarely change)
4386
- - ideas/ - Proposals (convert to tasks when ready)
4387
- - tasks/ - Actionable work with definitions of done
4388
- - facts/ - Documentation of current system state
4450
+ - principles/ - Guiding values (stable, rarely change)
4451
+ - ideas/ - Proposals (convert to tasks when ready)
4452
+ - tasks/ - Actionable work with definitions of done
4453
+ - facts/ - Documentation of current system state
4389
4454
 
4390
4455
  Workflow: Pick a task → implement it → delete the task file → commit atomically.
4391
4456
 
@@ -4407,7 +4472,7 @@ async function implementTask(dependencies) {
4407
4472
  }
4408
4473
 
4409
4474
  // lib/cli/commands/init.ts
4410
- var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
4475
+ var DUST_DIRECTORIES = ["principles", "ideas", "tasks", "facts", "config"];
4411
4476
  function generateSettings(cwd, fileSystem) {
4412
4477
  const dustCommand = detectDustCommand(cwd, fileSystem);
4413
4478
  const testCommand = detectTestCommand(cwd, fileSystem);
@@ -4439,7 +4504,7 @@ function agentsMdContent(dustCommand) {
4439
4504
  Always immediately run \`${dustCommand} agent\` when you start working in this repository.
4440
4505
  `;
4441
4506
  }
4442
- async function init2(dependencies) {
4507
+ async function init(dependencies) {
4443
4508
  const { context, fileSystem } = dependencies;
4444
4509
  const colors = getColors();
4445
4510
  const dustPath = `${context.cwd}/.dust`;
@@ -4509,35 +4574,35 @@ async function init2(dependencies) {
4509
4574
  context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Idea: friendly UI for non-technical users"`);
4510
4575
  context.stdout(` ${colors.cyan}>${colors.reset} ${runner} codex "Task: set up code coverage"`);
4511
4576
  context.stdout("");
4512
- context.stdout(`${colors.dim}If this is an existing codebase, you might want to backfill goals and facts:${colors.reset}`);
4513
- context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Add goals and facts based on the code in this repository"`);
4577
+ context.stdout(`${colors.dim}If this is an existing codebase, you might want to backfill principles and facts:${colors.reset}`);
4578
+ context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Add principles and facts based on the code in this repository"`);
4514
4579
  return { exitCode: 0 };
4515
4580
  }
4516
4581
 
4517
4582
  // lib/cli/commands/list.ts
4518
4583
  import { basename as basename2 } from "node:path";
4519
- var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
4584
+ var VALID_TYPES = ["tasks", "ideas", "principles", "facts"];
4520
4585
  var SECTION_HEADERS = {
4521
4586
  tasks: "\uD83D\uDCCB Tasks",
4522
4587
  ideas: "\uD83D\uDCA1 Ideas",
4523
- goals: "\uD83C\uDFAF Goals",
4588
+ principles: "\uD83C\uDFAF Principles",
4524
4589
  facts: "\uD83D\uDCC4 Facts"
4525
4590
  };
4526
4591
  var TYPE_EXPLANATIONS = {
4527
4592
  tasks: "Tasks are detailed work plans with dependencies and completion criteria. Each task describes a specific piece of work to be done.",
4528
4593
  ideas: "Ideas are future feature notes and proposals. Ideas capture possibilities that haven't yet been refined into actionable tasks.",
4529
- goals: "Goals are mission statements and guiding principles. Goals describe desired outcomes and values that inform decision-making.",
4594
+ principles: "Principles are guiding values and design constraints. Principles describe how decisions should be made and what matters most.",
4530
4595
  facts: "Facts are current state documentation. Facts capture how things work today, providing context for agents and contributors."
4531
4596
  };
4532
- async function buildGoalHierarchy(goalsPath, fileSystem) {
4533
- const files = await fileSystem.readdir(goalsPath);
4597
+ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
4598
+ const files = await fileSystem.readdir(principlesPath);
4534
4599
  const mdFiles = files.filter((f) => f.endsWith(".md"));
4535
4600
  const relationships = [];
4536
4601
  const titleMap = new Map;
4537
4602
  for (const file of mdFiles) {
4538
- const filePath = `${goalsPath}/${file}`;
4603
+ const filePath = `${principlesPath}/${file}`;
4539
4604
  const content = await fileSystem.readFile(filePath);
4540
- relationships.push(extractGoalRelationships(filePath, content));
4605
+ relationships.push(extractPrincipleRelationships(filePath, content));
4541
4606
  const title = extractTitle(content) || basename2(file, ".md");
4542
4607
  titleMap.set(filePath, title);
4543
4608
  }
@@ -4545,12 +4610,12 @@ async function buildGoalHierarchy(goalsPath, fileSystem) {
4545
4610
  for (const rel of relationships) {
4546
4611
  relMap.set(rel.filePath, rel);
4547
4612
  }
4548
- const rootGoals = relationships.filter((rel) => rel.parentGoals.length === 0);
4613
+ const rootPrinciples = relationships.filter((rel) => rel.parentPrinciples.length === 0);
4549
4614
  function buildNode(filePath) {
4550
4615
  const rel = relMap.get(filePath);
4551
4616
  const children = [];
4552
4617
  if (rel) {
4553
- for (const childPath of rel.subGoals) {
4618
+ for (const childPath of rel.subPrinciples) {
4554
4619
  children.push(buildNode(childPath));
4555
4620
  }
4556
4621
  }
@@ -4560,7 +4625,7 @@ async function buildGoalHierarchy(goalsPath, fileSystem) {
4560
4625
  children
4561
4626
  };
4562
4627
  }
4563
- return rootGoals.map((rel) => buildNode(rel.filePath));
4628
+ return rootPrinciples.map((rel) => buildNode(rel.filePath));
4564
4629
  }
4565
4630
  function renderHierarchy(nodes, output, prefix = "") {
4566
4631
  for (let i = 0;i < nodes.length; i++) {
@@ -4610,8 +4675,8 @@ async function list(dependencies) {
4610
4675
  context.stdout("");
4611
4676
  context.stdout(TYPE_EXPLANATIONS[type]);
4612
4677
  context.stdout("");
4613
- if (type === "goals") {
4614
- const hierarchy = await buildGoalHierarchy(dirPath, fileSystem);
4678
+ if (type === "principles") {
4679
+ const hierarchy = await buildPrincipleHierarchy(dirPath, fileSystem);
4615
4680
  if (hierarchy.length > 0) {
4616
4681
  context.stdout(`${colors.dim}Hierarchy:${colors.reset}`);
4617
4682
  renderHierarchy(hierarchy, (line) => context.stdout(line));
@@ -4774,37 +4839,68 @@ async function loopCodex(dependencies, loopDependencies = createCodexDependencie
4774
4839
  });
4775
4840
  }
4776
4841
 
4777
- // lib/cli/commands/new-goal.ts
4778
- function newGoalInstructions(vars) {
4779
- const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
4780
- return dedent`
4781
- ## Adding a New Goal
4782
-
4783
- Goals are guiding principles that persist across tasks. They define the "why" behind the work.
4784
-
4785
- ${intro}
4786
- 1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
4787
- 2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
4788
- 3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
4789
- 4. Write a clear description explaining:
4790
- - What this goal means in practice
4791
- - Why it matters for the project
4792
- - How to evaluate whether work supports this goal
4793
- 5. Run \`${vars.bin} lint\` to catch any formatting issues
4794
- 6. Create a single atomic commit with a message in the format "Add goal: <title>"
4795
- 7. Push your commit to the remote repository
4796
-
4797
- Goals should be:
4798
- - **Stable** - They rarely change once established
4799
- - **Actionable** - Tasks can be linked to them
4800
- - **Clear** - Anyone reading should understand what it means
4801
- `;
4842
+ // lib/cli/commands/migrate.ts
4843
+ async function scanMarkdownFiles(glob, dirPath) {
4844
+ const files = [];
4845
+ try {
4846
+ for await (const file of glob.scan(dirPath)) {
4847
+ if (file.endsWith(".md")) {
4848
+ files.push(`${dirPath}/${file}`);
4849
+ }
4850
+ }
4851
+ return files;
4852
+ } catch (error) {
4853
+ if (error.code === "ENOENT") {
4854
+ return [];
4855
+ }
4856
+ throw error;
4857
+ }
4802
4858
  }
4803
- async function newGoal(dependencies) {
4804
- const { context, settings } = dependencies;
4805
- const hooksInstalled = await manageGitHooks(dependencies);
4806
- const vars = templateVariables(settings, hooksInstalled);
4807
- context.stdout(newGoalInstructions(vars));
4859
+ async function migrate(dependencies) {
4860
+ const { context, fileSystem, globScanner } = dependencies;
4861
+ const colors = getColors();
4862
+ const dustPath = `${context.cwd}/.dust`;
4863
+ const goalsPath = `${dustPath}/goals`;
4864
+ const principlesPath = `${dustPath}/principles`;
4865
+ const dustExists = fileSystem.exists(dustPath);
4866
+ if (!dustExists) {
4867
+ context.stderr(`${colors.yellow}Error:${colors.reset} .dust directory not found. Run '${colors.cyan}dust init${colors.reset}' first.`);
4868
+ return { exitCode: 1 };
4869
+ }
4870
+ const goalsExists = fileSystem.exists(goalsPath);
4871
+ const principlesExists = fileSystem.exists(principlesPath);
4872
+ if (!goalsExists && principlesExists) {
4873
+ context.stdout(`${colors.green}✓${colors.reset} Already migrated - .dust/principles/ exists`);
4874
+ return { exitCode: 0 };
4875
+ }
4876
+ if (!goalsExists && !principlesExists) {
4877
+ context.stdout(`${colors.yellow}⚠️${colors.reset} No .dust/goals/ directory found. Creating .dust/principles/...`);
4878
+ await fileSystem.mkdir(principlesPath, { recursive: true });
4879
+ return { exitCode: 0 };
4880
+ }
4881
+ let updatedFilesCount = 0;
4882
+ context.stdout(`${colors.cyan}→${colors.reset} Renaming .dust/goals/ to .dust/principles/...`);
4883
+ await fileSystem.rename(goalsPath, principlesPath);
4884
+ context.stdout(`${colors.cyan}→${colors.reset} Updating references in markdown files...`);
4885
+ const markdownFiles = await scanMarkdownFiles(globScanner, dustPath);
4886
+ for (const filePath of markdownFiles) {
4887
+ const content = await fileSystem.readFile(filePath);
4888
+ let updated = content;
4889
+ updated = updated.replace(/## Goals\b/g, "## Principles");
4890
+ updated = updated.replace(/## Parent Goal\b/g, "## Parent Principle");
4891
+ updated = updated.replace(/## Sub-Goals\b/g, "## Sub-Principles");
4892
+ updated = updated.replace(/\.\.\/goals\//g, "../principles/");
4893
+ if (updated !== content) {
4894
+ await fileSystem.writeFile(filePath, updated);
4895
+ updatedFilesCount++;
4896
+ }
4897
+ }
4898
+ context.stdout("");
4899
+ context.stdout(`${colors.green}✓${colors.reset} Migration complete!`);
4900
+ context.stdout(` ${colors.dim}• Renamed .dust/goals/ → .dust/principles/${colors.reset}`);
4901
+ if (updatedFilesCount > 0) {
4902
+ context.stdout(` ${colors.dim}• Updated ${updatedFilesCount} markdown file(s)${colors.reset}`);
4903
+ }
4808
4904
  return { exitCode: 0 };
4809
4905
  }
4810
4906
 
@@ -4867,6 +4963,40 @@ async function newIdea(dependencies) {
4867
4963
  return { exitCode: 0 };
4868
4964
  }
4869
4965
 
4966
+ // lib/cli/commands/new-principle.ts
4967
+ function newPrincipleInstructions(vars) {
4968
+ const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
4969
+ return dedent`
4970
+ ## Adding a New Principle
4971
+
4972
+ Principles are guiding values that persist across tasks. They define the "why" behind the work.
4973
+
4974
+ ${intro}
4975
+ 1. Run \`${vars.bin} principles\` to see existing principles and avoid duplication
4976
+ 2. Create a new markdown file in \`.dust/principles/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
4977
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
4978
+ 4. Write a clear description explaining:
4979
+ - What this principle means in practice
4980
+ - Why it matters for the project
4981
+ - How to evaluate whether work supports this principle
4982
+ 5. Run \`${vars.bin} lint\` to catch any formatting issues
4983
+ 6. Create a single atomic commit with a message in the format "Add principle: <title>"
4984
+ 7. Push your commit to the remote repository
4985
+
4986
+ Principles should be:
4987
+ - **Stable** - They rarely change once established
4988
+ - **Actionable** - Tasks can be linked to them
4989
+ - **Clear** - Anyone reading should understand what it means
4990
+ `;
4991
+ }
4992
+ async function newPrinciple(dependencies) {
4993
+ const { context, settings } = dependencies;
4994
+ const hooksInstalled = await manageGitHooks(dependencies);
4995
+ const vars = templateVariables(settings, hooksInstalled);
4996
+ context.stdout(newPrincipleInstructions(vars));
4997
+ return { exitCode: 0 };
4998
+ }
4999
+
4870
5000
  // lib/cli/commands/new-task.ts
4871
5001
  function newTaskInstructions(vars) {
4872
5002
  const steps = [];
@@ -4893,7 +5023,7 @@ function newTaskInstructions(vars) {
4893
5023
  steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
4894
5024
  steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
4895
5025
  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.');
4896
- steps.push("7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)");
5026
+ steps.push("7. Add a `## Principles` section with links to relevant principles this task supports (e.g., `- [Principle Name](../principles/principle-name.md)`)");
4897
5027
  steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
4898
5028
  steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
4899
5029
  steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
@@ -4920,7 +5050,7 @@ async function newTask(dependencies) {
4920
5050
  async function pickTask(dependencies) {
4921
5051
  const { context, fileSystem, settings } = dependencies;
4922
5052
  await manageGitHooks(dependencies);
4923
- const result = await findUnblockedTasks(context.cwd, fileSystem);
5053
+ const result = await findUnblockedTasks(context.cwd, fileSystem, dependencies.directoryFileSorter);
4924
5054
  if (result.error) {
4925
5055
  context.stderr(`Error: ${result.error}`);
4926
5056
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -5073,8 +5203,8 @@ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process
5073
5203
  async function tasks(dependencies) {
5074
5204
  return list({ ...dependencies, arguments: ["tasks"] });
5075
5205
  }
5076
- async function goals(dependencies) {
5077
- return list({ ...dependencies, arguments: ["goals"] });
5206
+ async function principles(dependencies) {
5207
+ return list({ ...dependencies, arguments: ["principles"] });
5078
5208
  }
5079
5209
  async function ideas(dependencies) {
5080
5210
  return list({ ...dependencies, arguments: ["ideas"] });
@@ -5085,11 +5215,11 @@ async function facts(dependencies) {
5085
5215
 
5086
5216
  // lib/cli/main.ts
5087
5217
  var commandRegistry = {
5088
- init: init2,
5218
+ init,
5089
5219
  lint: lintMarkdown,
5090
5220
  list,
5091
5221
  tasks,
5092
- goals,
5222
+ principles,
5093
5223
  ideas,
5094
5224
  facts,
5095
5225
  next,
@@ -5099,17 +5229,17 @@ var commandRegistry = {
5099
5229
  bucket,
5100
5230
  focus,
5101
5231
  "new task": newTask,
5102
- "new goal": newGoal,
5232
+ "new principle": newPrinciple,
5103
5233
  "new idea": newIdea,
5104
5234
  "implement task": implementTask,
5105
5235
  "pick task": pickTask,
5106
5236
  "loop claude": loopClaude,
5107
5237
  "loop codex": loopCodex,
5108
5238
  "pre push": prePush,
5239
+ migrate,
5109
5240
  help
5110
5241
  };
5111
5242
  var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes(" "));
5112
- var HELP_TEXT = generateHelpText({ dustCommand: "dust" });
5113
5243
  function isHelpRequest(command) {
5114
5244
  return !command || command === "help" || command === "--help" || command === "-h";
5115
5245
  }
@@ -5129,7 +5259,7 @@ function resolveCommand(commandArguments) {
5129
5259
  return { command: null, remaining: commandArguments };
5130
5260
  }
5131
5261
  async function main(options) {
5132
- const { commandArguments, context, fileSystem, glob } = options;
5262
+ const { commandArguments, context, fileSystem, glob, directoryFileSorter } = options;
5133
5263
  const settings = await loadSettings(context.cwd, fileSystem);
5134
5264
  const helpText = generateHelpText(settings);
5135
5265
  if (isHelpRequest(commandArguments[0])) {
@@ -5147,7 +5277,8 @@ async function main(options) {
5147
5277
  context,
5148
5278
  fileSystem,
5149
5279
  globScanner: glob,
5150
- settings
5280
+ settings,
5281
+ directoryFileSorter
5151
5282
  };
5152
5283
  return runCommand(command, dependencies);
5153
5284
  }
@@ -5173,7 +5304,8 @@ function createFileSystem(primitives) {
5173
5304
  },
5174
5305
  getFileCreationTime: (path) => primitives.statSync(path).birthtimeMs,
5175
5306
  readdir: (path) => primitives.readdir(path),
5176
- chmod: (path, mode) => primitives.chmod(path, mode)
5307
+ chmod: (path, mode) => primitives.chmod(path, mode),
5308
+ rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
5177
5309
  };
5178
5310
  }
5179
5311
  function createGlobScanner(readdir2) {
@@ -5189,6 +5321,7 @@ function createGlobScanner(readdir2) {
5189
5321
  async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
5190
5322
  const fileSystem = createFileSystem(fsPrimitives);
5191
5323
  const glob = createGlobScanner(fsPrimitives.readdir);
5324
+ const directoryFileSorter = createGitDirectoryFileSorter(defaultGitRunner);
5192
5325
  const result = await main({
5193
5326
  commandArguments: processPrimitives.argv.slice(2),
5194
5327
  context: {
@@ -5198,13 +5331,14 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
5198
5331
  stderr: consolePrimitives.error
5199
5332
  },
5200
5333
  fileSystem,
5201
- glob
5334
+ glob,
5335
+ directoryFileSorter
5202
5336
  });
5203
5337
  processPrimitives.exit(result.exitCode);
5204
5338
  }
5205
5339
 
5206
5340
  // lib/cli/run.ts
5207
- await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2 }, {
5341
+ await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2, rename }, {
5208
5342
  argv: process.argv,
5209
5343
  cwd: () => process.cwd(),
5210
5344
  exit: (code) => {