@joshski/dust 0.1.58 → 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.
@@ -24,6 +24,7 @@ export interface FileSystem {
24
24
  chmod: (path: string, mode: number) => Promise<void>;
25
25
  isDirectory: (path: string) => boolean;
26
26
  getFileCreationTime: (path: string) => number;
27
+ rename: (oldPath: string, newPath: string) => Promise<void>;
27
28
  }
28
29
  export interface GlobScanner {
29
30
  scan: (dir: string) => AsyncIterable<string>;
@@ -41,6 +42,7 @@ export interface DustSettings {
41
42
  eventsUrl?: string;
42
43
  extraDirectories?: string[];
43
44
  }
45
+ export type DirectoryFileSorter = (dir: string, files: string[]) => Promise<string[]>;
44
46
  /**
45
47
  * Dependencies passed to all CLI commands
46
48
  */
@@ -50,4 +52,5 @@ export interface CommandDependencies {
50
52
  fileSystem: FileSystem;
51
53
  globScanner: GlobScanner;
52
54
  settings: DustSettings;
55
+ directoryFileSorter?: DirectoryFileSorter;
53
56
  }
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,
@@ -1874,7 +1900,7 @@ function extractBlockedBy(content) {
1874
1900
  }
1875
1901
  return blockers;
1876
1902
  }
1877
- async function findUnblockedTasks(cwd, fileSystem) {
1903
+ async function findUnblockedTasks(cwd, fileSystem, directoryFileSorter) {
1878
1904
  const dustPath = `${cwd}/.dust`;
1879
1905
  if (!fileSystem.exists(dustPath)) {
1880
1906
  return { error: ".dust directory not found", tasks: [] };
@@ -1884,11 +1910,16 @@ async function findUnblockedTasks(cwd, fileSystem) {
1884
1910
  return { tasks: [] };
1885
1911
  }
1886
1912
  const files = await fileSystem.readdir(tasksPath);
1887
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort((a, b) => {
1888
- const aTime = fileSystem.getFileCreationTime(`${tasksPath}/${a}`);
1889
- const bTime = fileSystem.getFileCreationTime(`${tasksPath}/${b}`);
1890
- return aTime - bTime;
1891
- });
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
+ }
1892
1923
  if (mdFiles.length === 0) {
1893
1924
  return { tasks: [] };
1894
1925
  }
@@ -1924,8 +1955,8 @@ function printTaskList(context, tasks) {
1924
1955
  }
1925
1956
  }
1926
1957
  async function next(dependencies) {
1927
- const { context, fileSystem } = dependencies;
1928
- const result = await findUnblockedTasks(context.cwd, fileSystem);
1958
+ const { context, fileSystem, directoryFileSorter } = dependencies;
1959
+ const result = await findUnblockedTasks(context.cwd, fileSystem, directoryFileSorter);
1929
1960
  if (result.error) {
1930
1961
  context.stderr(`Error: ${result.error}`);
1931
1962
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -2059,8 +2090,8 @@ async function gitPull(cwd, spawn) {
2059
2090
  });
2060
2091
  }
2061
2092
  async function findAvailableTasks(dependencies) {
2062
- const { context, fileSystem } = dependencies;
2063
- const result = await findUnblockedTasks(context.cwd, fileSystem);
2093
+ const { context, fileSystem, directoryFileSorter } = dependencies;
2094
+ const result = await findUnblockedTasks(context.cwd, fileSystem, directoryFileSorter);
2064
2095
  return result.tasks;
2065
2096
  }
2066
2097
  async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, options = {}) {
@@ -3047,7 +3078,8 @@ function createDefaultBucketDependencies() {
3047
3078
  writeFile: (path, content) => writeFile(path, content, "utf8"),
3048
3079
  mkdir: (path, options) => mkdir(path, options).then(() => {}),
3049
3080
  readdir: (path) => readdir(path),
3050
- 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))
3051
3083
  };
3052
3084
  return {
3053
3085
  spawn: nodeSpawn3,
@@ -3488,7 +3520,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3488
3520
  import { join as join10 } from "node:path";
3489
3521
 
3490
3522
  // lib/lint/validators/content-validator.ts
3491
- var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
3523
+ var REQUIRED_HEADINGS = [
3524
+ "## Principles",
3525
+ "## Blocked By",
3526
+ "## Definition of Done"
3527
+ ];
3492
3528
  var MAX_OPENING_SENTENCE_LENGTH = 150;
3493
3529
  var NON_IMPERATIVE_STARTERS = new Set([
3494
3530
  "the",
@@ -3557,7 +3593,7 @@ function validateTaskHeadings(filePath, content) {
3557
3593
  }
3558
3594
 
3559
3595
  // lib/lint/validators/directory-validator.ts
3560
- var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
3596
+ var EXPECTED_DIRECTORIES = ["principles", "ideas", "tasks", "facts", "config"];
3561
3597
  async function validateContentDirectoryFiles(dirPath, fileSystem) {
3562
3598
  const violations = [];
3563
3599
  let entries;
@@ -3655,117 +3691,6 @@ function validateTitleFilenameMatch(filePath, content) {
3655
3691
  return null;
3656
3692
  }
3657
3693
 
3658
- // lib/lint/validators/goal-hierarchy.ts
3659
- import { dirname as dirname4, resolve } from "node:path";
3660
- var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
3661
- function validateGoalHierarchySections(filePath, content) {
3662
- const violations = [];
3663
- for (const heading of REQUIRED_GOAL_HEADINGS) {
3664
- if (!content.includes(heading)) {
3665
- violations.push({
3666
- file: filePath,
3667
- message: `Missing required heading: "${heading}"`
3668
- });
3669
- }
3670
- }
3671
- return violations;
3672
- }
3673
- function extractGoalRelationships(filePath, content) {
3674
- const lines = content.split(`
3675
- `);
3676
- const fileDir = dirname4(filePath);
3677
- const parentGoals = [];
3678
- const subGoals = [];
3679
- let currentSection = null;
3680
- for (const line of lines) {
3681
- if (line.startsWith("## ")) {
3682
- currentSection = line;
3683
- continue;
3684
- }
3685
- if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
3686
- continue;
3687
- }
3688
- const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
3689
- let match = linkPattern.exec(line);
3690
- while (match) {
3691
- const linkTarget = match[2];
3692
- if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
3693
- const targetPath = linkTarget.split("#")[0];
3694
- const resolvedPath = resolve(fileDir, targetPath);
3695
- if (resolvedPath.includes("/.dust/goals/")) {
3696
- if (currentSection === "## Parent Goal") {
3697
- parentGoals.push(resolvedPath);
3698
- } else {
3699
- subGoals.push(resolvedPath);
3700
- }
3701
- }
3702
- }
3703
- match = linkPattern.exec(line);
3704
- }
3705
- }
3706
- return { filePath, parentGoals, subGoals };
3707
- }
3708
- function validateBidirectionalLinks(allGoalRelationships) {
3709
- const violations = [];
3710
- const relationshipMap = new Map;
3711
- for (const rel of allGoalRelationships) {
3712
- relationshipMap.set(rel.filePath, rel);
3713
- }
3714
- for (const rel of allGoalRelationships) {
3715
- for (const parentPath of rel.parentGoals) {
3716
- const parentRel = relationshipMap.get(parentPath);
3717
- if (parentRel && !parentRel.subGoals.includes(rel.filePath)) {
3718
- violations.push({
3719
- file: rel.filePath,
3720
- message: `Parent goal "${parentPath}" does not list this goal as a sub-goal`
3721
- });
3722
- }
3723
- }
3724
- for (const subGoalPath of rel.subGoals) {
3725
- const subGoalRel = relationshipMap.get(subGoalPath);
3726
- if (subGoalRel && !subGoalRel.parentGoals.includes(rel.filePath)) {
3727
- violations.push({
3728
- file: rel.filePath,
3729
- message: `Sub-goal "${subGoalPath}" does not list this goal as its parent`
3730
- });
3731
- }
3732
- }
3733
- }
3734
- return violations;
3735
- }
3736
- function validateNoCycles(allGoalRelationships) {
3737
- const violations = [];
3738
- const relationshipMap = new Map;
3739
- for (const rel of allGoalRelationships) {
3740
- relationshipMap.set(rel.filePath, rel);
3741
- }
3742
- for (const rel of allGoalRelationships) {
3743
- const visited = new Set;
3744
- const path = [];
3745
- let current = rel.filePath;
3746
- while (current) {
3747
- if (visited.has(current)) {
3748
- const cycleStart = path.indexOf(current);
3749
- const cyclePath = path.slice(cycleStart).concat(current);
3750
- violations.push({
3751
- file: rel.filePath,
3752
- message: `Cycle detected in goal hierarchy: ${cyclePath.join(" -> ")}`
3753
- });
3754
- break;
3755
- }
3756
- visited.add(current);
3757
- path.push(current);
3758
- const currentRel = relationshipMap.get(current);
3759
- if (currentRel && currentRel.parentGoals.length > 0) {
3760
- current = currentRel.parentGoals[0];
3761
- } else {
3762
- current = null;
3763
- }
3764
- }
3765
- }
3766
- return violations;
3767
- }
3768
-
3769
3694
  // lib/lint/validators/idea-validator.ts
3770
3695
  function validateIdeaOpenQuestions(filePath, content) {
3771
3696
  const violations = [];
@@ -3882,12 +3807,12 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
3882
3807
  }
3883
3808
 
3884
3809
  // lib/lint/validators/link-validator.ts
3885
- import { dirname as dirname5, resolve as resolve2 } from "node:path";
3810
+ import { dirname as dirname4, resolve } from "node:path";
3886
3811
  var SEMANTIC_RULES = [
3887
3812
  {
3888
- section: "## Goals",
3889
- requiredPath: "/.dust/goals/",
3890
- description: "goal"
3813
+ section: "## Principles",
3814
+ requiredPath: "/.dust/principles/",
3815
+ description: "principle"
3891
3816
  },
3892
3817
  {
3893
3818
  section: "## Blocked By",
@@ -3899,7 +3824,7 @@ function validateLinks(filePath, content, fileSystem) {
3899
3824
  const violations = [];
3900
3825
  const lines = content.split(`
3901
3826
  `);
3902
- const fileDir = dirname5(filePath);
3827
+ const fileDir = dirname4(filePath);
3903
3828
  for (let i = 0;i < lines.length; i++) {
3904
3829
  const line = lines[i];
3905
3830
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3908,7 +3833,7 @@ function validateLinks(filePath, content, fileSystem) {
3908
3833
  const linkTarget = match[2];
3909
3834
  if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
3910
3835
  const targetPath = linkTarget.split("#")[0];
3911
- const resolvedPath = resolve2(fileDir, targetPath);
3836
+ const resolvedPath = resolve(fileDir, targetPath);
3912
3837
  if (!fileSystem.exists(resolvedPath)) {
3913
3838
  violations.push({
3914
3839
  file: filePath,
@@ -3926,7 +3851,7 @@ function validateSemanticLinks(filePath, content) {
3926
3851
  const violations = [];
3927
3852
  const lines = content.split(`
3928
3853
  `);
3929
- const fileDir = dirname5(filePath);
3854
+ const fileDir = dirname4(filePath);
3930
3855
  let currentSection = null;
3931
3856
  for (let i = 0;i < lines.length; i++) {
3932
3857
  const line = lines[i];
@@ -3960,7 +3885,7 @@ function validateSemanticLinks(filePath, content) {
3960
3885
  continue;
3961
3886
  }
3962
3887
  const targetPath = linkTarget.split("#")[0];
3963
- const resolvedPath = resolve2(fileDir, targetPath);
3888
+ const resolvedPath = resolve(fileDir, targetPath);
3964
3889
  if (!resolvedPath.includes(rule.requiredPath)) {
3965
3890
  violations.push({
3966
3891
  file: filePath,
@@ -3973,11 +3898,11 @@ function validateSemanticLinks(filePath, content) {
3973
3898
  }
3974
3899
  return violations;
3975
3900
  }
3976
- function validateGoalHierarchyLinks(filePath, content) {
3901
+ function validatePrincipleHierarchyLinks(filePath, content) {
3977
3902
  const violations = [];
3978
3903
  const lines = content.split(`
3979
3904
  `);
3980
- const fileDir = dirname5(filePath);
3905
+ const fileDir = dirname4(filePath);
3981
3906
  let currentSection = null;
3982
3907
  for (let i = 0;i < lines.length; i++) {
3983
3908
  const line = lines[i];
@@ -3985,7 +3910,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3985
3910
  currentSection = line;
3986
3911
  continue;
3987
3912
  }
3988
- if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
3913
+ if (currentSection !== "## Parent Principle" && currentSection !== "## Sub-Principles") {
3989
3914
  continue;
3990
3915
  }
3991
3916
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3995,7 +3920,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3995
3920
  if (linkTarget.startsWith("#")) {
3996
3921
  violations.push({
3997
3922
  file: filePath,
3998
- 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}"`,
3999
3924
  line: i + 1
4000
3925
  });
4001
3926
  match = linkPattern.exec(line);
@@ -4004,18 +3929,18 @@ function validateGoalHierarchyLinks(filePath, content) {
4004
3929
  if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
4005
3930
  violations.push({
4006
3931
  file: filePath,
4007
- 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}"`,
4008
3933
  line: i + 1
4009
3934
  });
4010
3935
  match = linkPattern.exec(line);
4011
3936
  continue;
4012
3937
  }
4013
3938
  const targetPath = linkTarget.split("#")[0];
4014
- const resolvedPath = resolve2(fileDir, targetPath);
4015
- if (!resolvedPath.includes("/.dust/goals/")) {
3939
+ const resolvedPath = resolve(fileDir, targetPath);
3940
+ if (!resolvedPath.includes("/.dust/principles/")) {
4016
3941
  violations.push({
4017
3942
  file: filePath,
4018
- message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
3943
+ message: `Link in "${currentSection}" must point to a principle file: "${linkTarget}"`,
4019
3944
  line: i + 1
4020
3945
  });
4021
3946
  }
@@ -4025,6 +3950,117 @@ function validateGoalHierarchyLinks(filePath, content) {
4025
3950
  return violations;
4026
3951
  }
4027
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
+
4028
4064
  // lib/cli/commands/lint-markdown.ts
4029
4065
  async function safeScanDir(glob, dirPath) {
4030
4066
  const files = [];
@@ -4085,7 +4121,7 @@ async function lintMarkdown(dependencies) {
4085
4121
  }
4086
4122
  }
4087
4123
  }
4088
- const contentDirs = ["goals", "facts", "ideas", "tasks"];
4124
+ const contentDirs = ["principles", "facts", "ideas", "tasks"];
4089
4125
  context.stdout("Validating content files...");
4090
4126
  for (const dir of contentDirs) {
4091
4127
  const dirPath = `${dustPath}/${dir}`;
@@ -4174,15 +4210,15 @@ async function lintMarkdown(dependencies) {
4174
4210
  }
4175
4211
  }
4176
4212
  }
4177
- const goalsPath = `${dustPath}/goals`;
4178
- const { files: goalFiles } = await safeScanDir(glob, goalsPath);
4179
- if (goalFiles.length > 0) {
4180
- context.stdout("Validating goal hierarchy in .dust/goals/...");
4181
- const allGoalRelationships = [];
4182
- 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) {
4183
4219
  if (!file.endsWith(".md"))
4184
4220
  continue;
4185
- const filePath = `${goalsPath}/${file}`;
4221
+ const filePath = `${principlesPath}/${file}`;
4186
4222
  let content;
4187
4223
  try {
4188
4224
  content = await fileSystem.readFile(filePath);
@@ -4192,12 +4228,12 @@ async function lintMarkdown(dependencies) {
4192
4228
  }
4193
4229
  throw error;
4194
4230
  }
4195
- violations.push(...validateGoalHierarchySections(filePath, content));
4196
- violations.push(...validateGoalHierarchyLinks(filePath, content));
4197
- allGoalRelationships.push(extractGoalRelationships(filePath, content));
4231
+ violations.push(...validatePrincipleHierarchySections(filePath, content));
4232
+ violations.push(...validatePrincipleHierarchyLinks(filePath, content));
4233
+ allPrincipleRelationships.push(extractPrincipleRelationships(filePath, content));
4198
4234
  }
4199
- violations.push(...validateBidirectionalLinks(allGoalRelationships));
4200
- violations.push(...validateNoCycles(allGoalRelationships));
4235
+ violations.push(...validateBidirectionalLinks(allPrincipleRelationships));
4236
+ violations.push(...validateNoCycles(allPrincipleRelationships));
4201
4237
  }
4202
4238
  if (violations.length === 0) {
4203
4239
  context.stdout("All validations passed!");
@@ -4390,10 +4426,10 @@ function generateHelpText(settings) {
4390
4426
  Commands:
4391
4427
  init Initialize a new Dust repository
4392
4428
  lint Run lint checks on .dust/ files
4393
- list List all items (tasks, ideas, goals, facts)
4429
+ list List all items (tasks, ideas, principles, facts)
4394
4430
  tasks List tasks (actionable work with definitions of done)
4395
4431
  ideas List ideas (vague proposals, convert to tasks when ready)
4396
- goals List goals (guiding principles, stable, rarely change)
4432
+ principles List principles (guiding values, stable, rarely change)
4397
4433
  facts List facts (documentation of current system state)
4398
4434
  next Show tasks ready to work on (not blocked)
4399
4435
  check Run project-defined quality gate hook
@@ -4402,7 +4438,7 @@ function generateHelpText(settings) {
4402
4438
  pick task Pick the next task to work on
4403
4439
  implement task Implement a task
4404
4440
  new task Create a new task
4405
- new goal Create a new goal
4441
+ new principle Create a new principle
4406
4442
  new idea Create a new idea
4407
4443
  loop claude Run continuous Claude iteration on tasks
4408
4444
  pre push Git pre-push hook validation
@@ -4411,10 +4447,10 @@ function generateHelpText(settings) {
4411
4447
  🤖 Agent Guide
4412
4448
 
4413
4449
  Dust is a lightweight planning system. The .dust/ directory contains:
4414
- - goals/ - Guiding principles (stable, rarely change)
4415
- - ideas/ - Proposals (convert to tasks when ready)
4416
- - tasks/ - Actionable work with definitions of done
4417
- - 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
4418
4454
 
4419
4455
  Workflow: Pick a task → implement it → delete the task file → commit atomically.
4420
4456
 
@@ -4436,7 +4472,7 @@ async function implementTask(dependencies) {
4436
4472
  }
4437
4473
 
4438
4474
  // lib/cli/commands/init.ts
4439
- var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
4475
+ var DUST_DIRECTORIES = ["principles", "ideas", "tasks", "facts", "config"];
4440
4476
  function generateSettings(cwd, fileSystem) {
4441
4477
  const dustCommand = detectDustCommand(cwd, fileSystem);
4442
4478
  const testCommand = detectTestCommand(cwd, fileSystem);
@@ -4538,35 +4574,35 @@ async function init(dependencies) {
4538
4574
  context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Idea: friendly UI for non-technical users"`);
4539
4575
  context.stdout(` ${colors.cyan}>${colors.reset} ${runner} codex "Task: set up code coverage"`);
4540
4576
  context.stdout("");
4541
- context.stdout(`${colors.dim}If this is an existing codebase, you might want to backfill goals and facts:${colors.reset}`);
4542
- 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"`);
4543
4579
  return { exitCode: 0 };
4544
4580
  }
4545
4581
 
4546
4582
  // lib/cli/commands/list.ts
4547
4583
  import { basename as basename2 } from "node:path";
4548
- var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
4584
+ var VALID_TYPES = ["tasks", "ideas", "principles", "facts"];
4549
4585
  var SECTION_HEADERS = {
4550
4586
  tasks: "\uD83D\uDCCB Tasks",
4551
4587
  ideas: "\uD83D\uDCA1 Ideas",
4552
- goals: "\uD83C\uDFAF Goals",
4588
+ principles: "\uD83C\uDFAF Principles",
4553
4589
  facts: "\uD83D\uDCC4 Facts"
4554
4590
  };
4555
4591
  var TYPE_EXPLANATIONS = {
4556
4592
  tasks: "Tasks are detailed work plans with dependencies and completion criteria. Each task describes a specific piece of work to be done.",
4557
4593
  ideas: "Ideas are future feature notes and proposals. Ideas capture possibilities that haven't yet been refined into actionable tasks.",
4558
- 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.",
4559
4595
  facts: "Facts are current state documentation. Facts capture how things work today, providing context for agents and contributors."
4560
4596
  };
4561
- async function buildGoalHierarchy(goalsPath, fileSystem) {
4562
- const files = await fileSystem.readdir(goalsPath);
4597
+ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
4598
+ const files = await fileSystem.readdir(principlesPath);
4563
4599
  const mdFiles = files.filter((f) => f.endsWith(".md"));
4564
4600
  const relationships = [];
4565
4601
  const titleMap = new Map;
4566
4602
  for (const file of mdFiles) {
4567
- const filePath = `${goalsPath}/${file}`;
4603
+ const filePath = `${principlesPath}/${file}`;
4568
4604
  const content = await fileSystem.readFile(filePath);
4569
- relationships.push(extractGoalRelationships(filePath, content));
4605
+ relationships.push(extractPrincipleRelationships(filePath, content));
4570
4606
  const title = extractTitle(content) || basename2(file, ".md");
4571
4607
  titleMap.set(filePath, title);
4572
4608
  }
@@ -4574,12 +4610,12 @@ async function buildGoalHierarchy(goalsPath, fileSystem) {
4574
4610
  for (const rel of relationships) {
4575
4611
  relMap.set(rel.filePath, rel);
4576
4612
  }
4577
- const rootGoals = relationships.filter((rel) => rel.parentGoals.length === 0);
4613
+ const rootPrinciples = relationships.filter((rel) => rel.parentPrinciples.length === 0);
4578
4614
  function buildNode(filePath) {
4579
4615
  const rel = relMap.get(filePath);
4580
4616
  const children = [];
4581
4617
  if (rel) {
4582
- for (const childPath of rel.subGoals) {
4618
+ for (const childPath of rel.subPrinciples) {
4583
4619
  children.push(buildNode(childPath));
4584
4620
  }
4585
4621
  }
@@ -4589,7 +4625,7 @@ async function buildGoalHierarchy(goalsPath, fileSystem) {
4589
4625
  children
4590
4626
  };
4591
4627
  }
4592
- return rootGoals.map((rel) => buildNode(rel.filePath));
4628
+ return rootPrinciples.map((rel) => buildNode(rel.filePath));
4593
4629
  }
4594
4630
  function renderHierarchy(nodes, output, prefix = "") {
4595
4631
  for (let i = 0;i < nodes.length; i++) {
@@ -4639,8 +4675,8 @@ async function list(dependencies) {
4639
4675
  context.stdout("");
4640
4676
  context.stdout(TYPE_EXPLANATIONS[type]);
4641
4677
  context.stdout("");
4642
- if (type === "goals") {
4643
- const hierarchy = await buildGoalHierarchy(dirPath, fileSystem);
4678
+ if (type === "principles") {
4679
+ const hierarchy = await buildPrincipleHierarchy(dirPath, fileSystem);
4644
4680
  if (hierarchy.length > 0) {
4645
4681
  context.stdout(`${colors.dim}Hierarchy:${colors.reset}`);
4646
4682
  renderHierarchy(hierarchy, (line) => context.stdout(line));
@@ -4803,37 +4839,68 @@ async function loopCodex(dependencies, loopDependencies = createCodexDependencie
4803
4839
  });
4804
4840
  }
4805
4841
 
4806
- // lib/cli/commands/new-goal.ts
4807
- function newGoalInstructions(vars) {
4808
- const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
4809
- return dedent`
4810
- ## Adding a New Goal
4811
-
4812
- Goals are guiding principles that persist across tasks. They define the "why" behind the work.
4813
-
4814
- ${intro}
4815
- 1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
4816
- 2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
4817
- 3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
4818
- 4. Write a clear description explaining:
4819
- - What this goal means in practice
4820
- - Why it matters for the project
4821
- - How to evaluate whether work supports this goal
4822
- 5. Run \`${vars.bin} lint\` to catch any formatting issues
4823
- 6. Create a single atomic commit with a message in the format "Add goal: <title>"
4824
- 7. Push your commit to the remote repository
4825
-
4826
- Goals should be:
4827
- - **Stable** - They rarely change once established
4828
- - **Actionable** - Tasks can be linked to them
4829
- - **Clear** - Anyone reading should understand what it means
4830
- `;
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
+ }
4831
4858
  }
4832
- async function newGoal(dependencies) {
4833
- const { context, settings } = dependencies;
4834
- const hooksInstalled = await manageGitHooks(dependencies);
4835
- const vars = templateVariables(settings, hooksInstalled);
4836
- 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
+ }
4837
4904
  return { exitCode: 0 };
4838
4905
  }
4839
4906
 
@@ -4896,6 +4963,40 @@ async function newIdea(dependencies) {
4896
4963
  return { exitCode: 0 };
4897
4964
  }
4898
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
+
4899
5000
  // lib/cli/commands/new-task.ts
4900
5001
  function newTaskInstructions(vars) {
4901
5002
  const steps = [];
@@ -4922,7 +5023,7 @@ function newTaskInstructions(vars) {
4922
5023
  steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
4923
5024
  steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
4924
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.');
4925
- 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)`)");
4926
5027
  steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
4927
5028
  steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
4928
5029
  steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
@@ -4949,7 +5050,7 @@ async function newTask(dependencies) {
4949
5050
  async function pickTask(dependencies) {
4950
5051
  const { context, fileSystem, settings } = dependencies;
4951
5052
  await manageGitHooks(dependencies);
4952
- const result = await findUnblockedTasks(context.cwd, fileSystem);
5053
+ const result = await findUnblockedTasks(context.cwd, fileSystem, dependencies.directoryFileSorter);
4953
5054
  if (result.error) {
4954
5055
  context.stderr(`Error: ${result.error}`);
4955
5056
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -5102,8 +5203,8 @@ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process
5102
5203
  async function tasks(dependencies) {
5103
5204
  return list({ ...dependencies, arguments: ["tasks"] });
5104
5205
  }
5105
- async function goals(dependencies) {
5106
- return list({ ...dependencies, arguments: ["goals"] });
5206
+ async function principles(dependencies) {
5207
+ return list({ ...dependencies, arguments: ["principles"] });
5107
5208
  }
5108
5209
  async function ideas(dependencies) {
5109
5210
  return list({ ...dependencies, arguments: ["ideas"] });
@@ -5118,7 +5219,7 @@ var commandRegistry = {
5118
5219
  lint: lintMarkdown,
5119
5220
  list,
5120
5221
  tasks,
5121
- goals,
5222
+ principles,
5122
5223
  ideas,
5123
5224
  facts,
5124
5225
  next,
@@ -5128,13 +5229,14 @@ var commandRegistry = {
5128
5229
  bucket,
5129
5230
  focus,
5130
5231
  "new task": newTask,
5131
- "new goal": newGoal,
5232
+ "new principle": newPrinciple,
5132
5233
  "new idea": newIdea,
5133
5234
  "implement task": implementTask,
5134
5235
  "pick task": pickTask,
5135
5236
  "loop claude": loopClaude,
5136
5237
  "loop codex": loopCodex,
5137
5238
  "pre push": prePush,
5239
+ migrate,
5138
5240
  help
5139
5241
  };
5140
5242
  var COMMANDS = Object.keys(commandRegistry).filter((cmd) => !cmd.includes(" "));
@@ -5157,7 +5259,7 @@ function resolveCommand(commandArguments) {
5157
5259
  return { command: null, remaining: commandArguments };
5158
5260
  }
5159
5261
  async function main(options) {
5160
- const { commandArguments, context, fileSystem, glob } = options;
5262
+ const { commandArguments, context, fileSystem, glob, directoryFileSorter } = options;
5161
5263
  const settings = await loadSettings(context.cwd, fileSystem);
5162
5264
  const helpText = generateHelpText(settings);
5163
5265
  if (isHelpRequest(commandArguments[0])) {
@@ -5175,7 +5277,8 @@ async function main(options) {
5175
5277
  context,
5176
5278
  fileSystem,
5177
5279
  globScanner: glob,
5178
- settings
5280
+ settings,
5281
+ directoryFileSorter
5179
5282
  };
5180
5283
  return runCommand(command, dependencies);
5181
5284
  }
@@ -5201,7 +5304,8 @@ function createFileSystem(primitives) {
5201
5304
  },
5202
5305
  getFileCreationTime: (path) => primitives.statSync(path).birthtimeMs,
5203
5306
  readdir: (path) => primitives.readdir(path),
5204
- chmod: (path, mode) => primitives.chmod(path, mode)
5307
+ chmod: (path, mode) => primitives.chmod(path, mode),
5308
+ rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
5205
5309
  };
5206
5310
  }
5207
5311
  function createGlobScanner(readdir2) {
@@ -5217,6 +5321,7 @@ function createGlobScanner(readdir2) {
5217
5321
  async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
5218
5322
  const fileSystem = createFileSystem(fsPrimitives);
5219
5323
  const glob = createGlobScanner(fsPrimitives.readdir);
5324
+ const directoryFileSorter = createGitDirectoryFileSorter(defaultGitRunner);
5220
5325
  const result = await main({
5221
5326
  commandArguments: processPrimitives.argv.slice(2),
5222
5327
  context: {
@@ -5226,13 +5331,14 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
5226
5331
  stderr: consolePrimitives.error
5227
5332
  },
5228
5333
  fileSystem,
5229
- glob
5334
+ glob,
5335
+ directoryFileSorter
5230
5336
  });
5231
5337
  processPrimitives.exit(result.exitCode);
5232
5338
  }
5233
5339
 
5234
5340
  // lib/cli/run.ts
5235
- 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 }, {
5236
5342
  argv: process.argv,
5237
5343
  cwd: () => process.cwd(),
5238
5344
  exit: (code) => {
@@ -86,7 +86,7 @@ ${renderResolvedQuestions(options.resolvedQuestions)}
86
86
 
87
87
  ${openingSentence}
88
88
  ${descriptionParagraph}${resolvedSection}
89
- ## Goals
89
+ ## Principles
90
90
 
91
91
  (none)
92
92
 
@@ -111,7 +111,7 @@ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSen
111
111
  return { filePath };
112
112
  }
113
113
  async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
114
- return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/goals/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
114
+ return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/principles/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
115
115
  "Idea is thoroughly researched with relevant codebase context",
116
116
  "Open questions are added for any ambiguous or underspecified aspects",
117
117
  "Open questions follow the required heading format and focus on high-value decisions",
@@ -119,9 +119,9 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
119
119
  ], { description });
120
120
  }
121
121
  async function decomposeIdea(fileSystem, dustPath, options) {
122
- return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/goals/\` to link relevant goals and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
122
+ return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/principles/\` to link relevant principles and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
123
123
  "One or more new tasks are created in .dust/tasks/",
124
- "Task's Goals section links to relevant goals from .dust/goals/",
124
+ "Task's Principles section links to relevant principles from .dust/principles/",
125
125
  "The original idea is deleted or updated to reflect remaining scope"
126
126
  ], {
127
127
  description: options.description,
@@ -145,13 +145,13 @@ async function createCaptureIdeaTask(fileSystem, dustPath, options) {
145
145
  const filePath2 = `${dustPath}/tasks/${filename2}`;
146
146
  const content2 = `# ${taskTitle2}
147
147
 
148
- Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
148
+ Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
149
149
 
150
150
  ## Idea Description
151
151
 
152
152
  ${description}
153
153
 
154
- ## Goals
154
+ ## Principles
155
155
 
156
156
  (none)
157
157
 
@@ -162,7 +162,7 @@ ${description}
162
162
  ## Definition of Done
163
163
 
164
164
  - [ ] One or more new tasks are created in \`.dust/tasks/\`
165
- - [ ] Tasks link to relevant goals from \`.dust/goals/\`
165
+ - [ ] Tasks link to relevant principles from \`.dust/principles/\`
166
166
  - [ ] Tasks are narrowly scoped vertical slices
167
167
  `;
168
168
  await fileSystem.writeFile(filePath2, content2);
@@ -175,13 +175,13 @@ ${description}
175
175
  const ideaPath = `.dust/ideas/${ideaFilename}`;
176
176
  const content = `# ${taskTitle}
177
177
 
178
- Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context.
178
+ Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context.
179
179
 
180
180
  ## Idea Description
181
181
 
182
182
  ${description}
183
183
 
184
- ## Goals
184
+ ## Principles
185
185
 
186
186
  (none)
187
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {