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