@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.
- package/dist/cli/types.d.ts +3 -0
- package/dist/dust.js +346 -240
- package/dist/workflow-tasks.js +9 -9
- package/package.json +1 -1
package/dist/cli/types.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
485
|
-
User has a
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
723
|
+
function ideasFromPrinciples() {
|
|
698
724
|
return dedent`
|
|
699
|
-
# Ideas from
|
|
725
|
+
# Ideas from Principles
|
|
700
726
|
|
|
701
|
-
Review \`.dust/
|
|
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
|
|
708
|
-
2. **Gap analysis** - Where does the codebase fall short of
|
|
709
|
-
3. **New opportunities** - What work would better achieve each
|
|
710
|
-
4. **
|
|
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
|
-
##
|
|
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
|
|
723
|
-
- [ ] Analyzed codebase for alignment with each
|
|
724
|
-
- [ ] Listed gaps between current state and
|
|
725
|
-
- [ ] Proposed new ideas for unmet or underserved
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
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 = [
|
|
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 = ["
|
|
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
|
|
3810
|
+
import { dirname as dirname4, resolve } from "node:path";
|
|
3886
3811
|
var SEMANTIC_RULES = [
|
|
3887
3812
|
{
|
|
3888
|
-
section: "##
|
|
3889
|
-
requiredPath: "/.dust/
|
|
3890
|
-
description: "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
3901
|
+
function validatePrincipleHierarchyLinks(filePath, content) {
|
|
3977
3902
|
const violations = [];
|
|
3978
3903
|
const lines = content.split(`
|
|
3979
3904
|
`);
|
|
3980
|
-
const fileDir =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4015
|
-
if (!resolvedPath.includes("/.dust/
|
|
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
|
|
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 = ["
|
|
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
|
|
4178
|
-
const { files:
|
|
4179
|
-
if (
|
|
4180
|
-
context.stdout("Validating
|
|
4181
|
-
const
|
|
4182
|
-
for (const file of
|
|
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 = `${
|
|
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(...
|
|
4196
|
-
violations.push(...
|
|
4197
|
-
|
|
4231
|
+
violations.push(...validatePrincipleHierarchySections(filePath, content));
|
|
4232
|
+
violations.push(...validatePrincipleHierarchyLinks(filePath, content));
|
|
4233
|
+
allPrincipleRelationships.push(extractPrincipleRelationships(filePath, content));
|
|
4198
4234
|
}
|
|
4199
|
-
violations.push(...validateBidirectionalLinks(
|
|
4200
|
-
violations.push(...validateNoCycles(
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
4415
|
-
- ideas/
|
|
4416
|
-
- tasks/
|
|
4417
|
-
- facts/
|
|
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 = ["
|
|
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
|
|
4542
|
-
context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Add
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4562
|
-
const files = await fileSystem.readdir(
|
|
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 = `${
|
|
4603
|
+
const filePath = `${principlesPath}/${file}`;
|
|
4568
4604
|
const content = await fileSystem.readFile(filePath);
|
|
4569
|
-
relationships.push(
|
|
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
|
|
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.
|
|
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
|
|
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 === "
|
|
4643
|
-
const hierarchy = await
|
|
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/
|
|
4807
|
-
function
|
|
4808
|
-
const
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
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
|
|
4833
|
-
const { context,
|
|
4834
|
-
const
|
|
4835
|
-
const
|
|
4836
|
-
|
|
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 `##
|
|
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
|
|
5106
|
-
return list({ ...dependencies, arguments: ["
|
|
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
|
-
|
|
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
|
|
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) => {
|
package/dist/workflow-tasks.js
CHANGED
|
@@ -86,7 +86,7 @@ ${renderResolvedQuestions(options.resolvedQuestions)}
|
|
|
86
86
|
|
|
87
87
|
${openingSentence}
|
|
88
88
|
${descriptionParagraph}${resolvedSection}
|
|
89
|
-
##
|
|
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/
|
|
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/
|
|
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
|
|
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/
|
|
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
|
-
##
|
|
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
|
|
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/
|
|
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
|
-
##
|
|
184
|
+
## Principles
|
|
185
185
|
|
|
186
186
|
(none)
|
|
187
187
|
|