@joshski/dust 0.1.42 → 0.1.43
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/agent-events.d.ts +5 -0
- package/dist/cli/types.d.ts +2 -0
- package/dist/dust.js +849 -100
- package/package.json +2 -3
- package/templates/agent-greeting.txt +0 -31
- package/templates/agent-implement-task.txt +0 -26
- package/templates/agent-new-goal.txt +0 -22
- package/templates/agent-new-idea.txt +0 -46
- package/templates/agent-new-task.txt +0 -29
- package/templates/agent-pick-task.txt +0 -7
- package/templates/agents-md.txt +0 -5
- package/templates/audits/dead-code.md +0 -30
- package/templates/audits/security-review.md +0 -30
- package/templates/audits/test-coverage.md +0 -29
- package/templates/claude-md.txt +0 -5
- package/templates/help.txt +0 -36
package/dist/dust.js
CHANGED
|
@@ -1,10 +1,149 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// lib/cli/run.ts
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
3
|
+
import { existsSync, statSync as statSync2 } from "node:fs";
|
|
4
4
|
import { chmod as chmod2, mkdir as mkdir2, readdir as readdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
5
5
|
|
|
6
6
|
// lib/config/settings.ts
|
|
7
7
|
import { join } from "node:path";
|
|
8
|
+
var KNOWN_SETTINGS_KEYS = new Set([
|
|
9
|
+
"dustCommand",
|
|
10
|
+
"checks",
|
|
11
|
+
"extraDirectories",
|
|
12
|
+
"installCommand",
|
|
13
|
+
"eventsUrl"
|
|
14
|
+
]);
|
|
15
|
+
var KNOWN_CHECK_KEYS = new Set([
|
|
16
|
+
"name",
|
|
17
|
+
"command",
|
|
18
|
+
"hints",
|
|
19
|
+
"timeoutMilliseconds"
|
|
20
|
+
]);
|
|
21
|
+
function validateSettingsJson(content) {
|
|
22
|
+
const violations = [];
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(content);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
violations.push({
|
|
28
|
+
message: `Invalid JSON: ${error.message}`
|
|
29
|
+
});
|
|
30
|
+
return violations;
|
|
31
|
+
}
|
|
32
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
33
|
+
violations.push({
|
|
34
|
+
message: "settings.json must be a JSON object"
|
|
35
|
+
});
|
|
36
|
+
return violations;
|
|
37
|
+
}
|
|
38
|
+
const settings = parsed;
|
|
39
|
+
for (const key of Object.keys(settings)) {
|
|
40
|
+
if (!KNOWN_SETTINGS_KEYS.has(key)) {
|
|
41
|
+
violations.push({
|
|
42
|
+
message: `Unknown key "${key}" in settings.json. Known keys: ${[...KNOWN_SETTINGS_KEYS].sort().join(", ")}`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ("checks" in settings) {
|
|
47
|
+
if (!Array.isArray(settings.checks)) {
|
|
48
|
+
violations.push({
|
|
49
|
+
message: '"checks" must be an array'
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
for (let i = 0;i < settings.checks.length; i++) {
|
|
53
|
+
const check = settings.checks[i];
|
|
54
|
+
const checkPath = `checks[${i}]`;
|
|
55
|
+
if (typeof check === "string") {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (typeof check !== "object" || check === null || Array.isArray(check)) {
|
|
59
|
+
violations.push({
|
|
60
|
+
message: `${checkPath} must be a string or object`
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const checkObj = check;
|
|
65
|
+
for (const key of Object.keys(checkObj)) {
|
|
66
|
+
if (!KNOWN_CHECK_KEYS.has(key)) {
|
|
67
|
+
violations.push({
|
|
68
|
+
message: `Unknown key "${key}" in ${checkPath}. Known keys: ${[...KNOWN_CHECK_KEYS].sort().join(", ")}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!("name" in checkObj)) {
|
|
73
|
+
violations.push({
|
|
74
|
+
message: `${checkPath} is missing required field "name"`
|
|
75
|
+
});
|
|
76
|
+
} else if (typeof checkObj.name !== "string") {
|
|
77
|
+
violations.push({
|
|
78
|
+
message: `${checkPath}.name must be a string`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!("command" in checkObj)) {
|
|
82
|
+
violations.push({
|
|
83
|
+
message: `${checkPath} is missing required field "command"`
|
|
84
|
+
});
|
|
85
|
+
} else if (typeof checkObj.command !== "string") {
|
|
86
|
+
violations.push({
|
|
87
|
+
message: `${checkPath}.command must be a string`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if ("hints" in checkObj) {
|
|
91
|
+
if (!Array.isArray(checkObj.hints)) {
|
|
92
|
+
violations.push({
|
|
93
|
+
message: `${checkPath}.hints must be an array of strings`
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
for (let j = 0;j < checkObj.hints.length; j++) {
|
|
97
|
+
if (typeof checkObj.hints[j] !== "string") {
|
|
98
|
+
violations.push({
|
|
99
|
+
message: `${checkPath}.hints[${j}] must be a string`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if ("timeoutMilliseconds" in checkObj) {
|
|
106
|
+
if (typeof checkObj.timeoutMilliseconds !== "number" || checkObj.timeoutMilliseconds <= 0) {
|
|
107
|
+
violations.push({
|
|
108
|
+
message: `${checkPath}.timeoutMilliseconds must be a positive number`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if ("extraDirectories" in settings) {
|
|
116
|
+
if (!Array.isArray(settings.extraDirectories)) {
|
|
117
|
+
violations.push({
|
|
118
|
+
message: '"extraDirectories" must be an array of strings'
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
for (let i = 0;i < settings.extraDirectories.length; i++) {
|
|
122
|
+
if (typeof settings.extraDirectories[i] !== "string") {
|
|
123
|
+
violations.push({
|
|
124
|
+
message: `extraDirectories[${i}] must be a string`
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if ("dustCommand" in settings && typeof settings.dustCommand !== "string") {
|
|
131
|
+
violations.push({
|
|
132
|
+
message: '"dustCommand" must be a string'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if ("installCommand" in settings && typeof settings.installCommand !== "string") {
|
|
136
|
+
violations.push({
|
|
137
|
+
message: '"installCommand" must be a string'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if ("eventsUrl" in settings && typeof settings.eventsUrl !== "string") {
|
|
141
|
+
violations.push({
|
|
142
|
+
message: '"eventsUrl" must be a string'
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return violations;
|
|
146
|
+
}
|
|
8
147
|
var DEFAULT_SETTINGS = {
|
|
9
148
|
dustCommand: "npx dust",
|
|
10
149
|
installCommand: "npm install"
|
|
@@ -110,36 +249,18 @@ async function loadSettings(cwd, fileSystem) {
|
|
|
110
249
|
}
|
|
111
250
|
}
|
|
112
251
|
|
|
113
|
-
// lib/cli/
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
function processConditionals(content, variables) {
|
|
123
|
-
let result = content.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, varName, block) => {
|
|
124
|
-
return isTruthy(variables[varName]) ? block : "";
|
|
125
|
-
});
|
|
126
|
-
result = result.replace(/\{\{#unless (\w+)\}\}([\s\S]*?)\{\{\/unless\}\}/g, (_, varName, block) => {
|
|
127
|
-
return !isTruthy(variables[varName]) ? block : "";
|
|
128
|
-
});
|
|
129
|
-
return result;
|
|
130
|
-
}
|
|
131
|
-
function loadTemplate(name, variables = {}) {
|
|
132
|
-
const templatePath = join2(templatesDir, `${name}.txt`);
|
|
133
|
-
let content = readFileSync(templatePath, "utf-8");
|
|
134
|
-
content = processConditionals(content, variables);
|
|
135
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
136
|
-
content = content.replaceAll(`{{${key}}}`, value);
|
|
137
|
-
}
|
|
138
|
-
return content;
|
|
252
|
+
// lib/cli/dedent.ts
|
|
253
|
+
function dedent(strings, ...values) {
|
|
254
|
+
const result = strings.reduce((acc, part, index) => acc + part + (values[index] ?? ""), "");
|
|
255
|
+
const lines = result.split(`
|
|
256
|
+
`);
|
|
257
|
+
const indent = lines.filter((line) => line.trim()).reduce((min, line) => Math.min(min, line.match(/^\s*/)[0].length), Number.POSITIVE_INFINITY);
|
|
258
|
+
return lines.map((line) => line.slice(indent)).join(`
|
|
259
|
+
`).trim();
|
|
139
260
|
}
|
|
140
261
|
|
|
141
262
|
// lib/cli/commands/agent-shared.ts
|
|
142
|
-
import { join as
|
|
263
|
+
import { join as join3 } from "node:path";
|
|
143
264
|
|
|
144
265
|
// lib/agents/detection.ts
|
|
145
266
|
function detectAgent(env = process.env) {
|
|
@@ -156,7 +277,7 @@ function detectAgent(env = process.env) {
|
|
|
156
277
|
}
|
|
157
278
|
|
|
158
279
|
// lib/git/hooks.ts
|
|
159
|
-
import { join as
|
|
280
|
+
import { join as join2 } from "node:path";
|
|
160
281
|
var DUST_HOOK_START = "# BEGIN DUST HOOK";
|
|
161
282
|
var DUST_HOOK_END = "# END DUST HOOK";
|
|
162
283
|
function generateHookContent(dustCommand) {
|
|
@@ -189,9 +310,9 @@ function removeDustSection(content) {
|
|
|
189
310
|
`).trim();
|
|
190
311
|
}
|
|
191
312
|
function createHooksManager(cwd, fileSystem, settings) {
|
|
192
|
-
const gitDir =
|
|
193
|
-
const hooksDir =
|
|
194
|
-
const prePushPath =
|
|
313
|
+
const gitDir = join2(cwd, ".git");
|
|
314
|
+
const hooksDir = join2(gitDir, "hooks");
|
|
315
|
+
const prePushPath = join2(hooksDir, "pre-push");
|
|
195
316
|
return {
|
|
196
317
|
isGitRepo: () => fileSystem.exists(gitDir),
|
|
197
318
|
isHookInstalled: async () => {
|
|
@@ -286,7 +407,7 @@ ${newHookContent}
|
|
|
286
407
|
|
|
287
408
|
// lib/cli/commands/agent-shared.ts
|
|
288
409
|
async function loadAgentInstructions(cwd, fileSystem, agentType) {
|
|
289
|
-
const instructionsPath =
|
|
410
|
+
const instructionsPath = join3(cwd, ".dust", "config", "agents", `${agentType}.md`);
|
|
290
411
|
if (!fileSystem.exists(instructionsPath)) {
|
|
291
412
|
return "";
|
|
292
413
|
}
|
|
@@ -297,23 +418,27 @@ async function loadAgentInstructions(cwd, fileSystem, agentType) {
|
|
|
297
418
|
return "";
|
|
298
419
|
}
|
|
299
420
|
}
|
|
300
|
-
function templateVariables(settings, hooksInstalled, env = process.env) {
|
|
421
|
+
function templateVariables(settings, hooksInstalled, env = process.env, options) {
|
|
301
422
|
const agent = detectAgent(env);
|
|
423
|
+
const hasIdeaFile = options?.hasIdeaFile ?? true;
|
|
302
424
|
return {
|
|
303
425
|
bin: settings.dustCommand,
|
|
304
426
|
agentName: agent.name,
|
|
305
|
-
hooksInstalled
|
|
306
|
-
isClaudeCodeWeb: agent.type === "claude-code-web"
|
|
427
|
+
hooksInstalled,
|
|
428
|
+
isClaudeCodeWeb: agent.type === "claude-code-web",
|
|
429
|
+
hasIdeaFile
|
|
307
430
|
};
|
|
308
431
|
}
|
|
309
|
-
async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env) {
|
|
432
|
+
async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env, options) {
|
|
310
433
|
const agent = detectAgent(env);
|
|
311
434
|
const agentInstructions = await loadAgentInstructions(cwd, fileSystem, agent.type);
|
|
435
|
+
const hasIdeaFile = options?.hasIdeaFile ?? true;
|
|
312
436
|
return {
|
|
313
437
|
bin: settings.dustCommand,
|
|
314
438
|
agentName: agent.name,
|
|
315
|
-
hooksInstalled
|
|
316
|
-
isClaudeCodeWeb: agent.type === "claude-code-web"
|
|
439
|
+
hooksInstalled,
|
|
440
|
+
isClaudeCodeWeb: agent.type === "claude-code-web",
|
|
441
|
+
hasIdeaFile,
|
|
317
442
|
agentInstructions
|
|
318
443
|
};
|
|
319
444
|
}
|
|
@@ -336,6 +461,39 @@ async function manageGitHooks(dependencies) {
|
|
|
336
461
|
}
|
|
337
462
|
|
|
338
463
|
// lib/cli/commands/agent.ts
|
|
464
|
+
function agentGreeting(vars) {
|
|
465
|
+
const instructions = vars.agentInstructions ? `
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
${vars.agentInstructions}` : "";
|
|
469
|
+
return dedent`
|
|
470
|
+
🤖 Hello ${vars.agentName}, welcome to dust!
|
|
471
|
+
|
|
472
|
+
CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
|
|
473
|
+
|
|
474
|
+
Determine the user's intent and run the matching command NOW:
|
|
475
|
+
|
|
476
|
+
1. **Pick up work from the backlog** → \`${vars.bin} pick task\`
|
|
477
|
+
User wants to start working. Examples: "work", "go", "pick a task", "what's next?"
|
|
478
|
+
|
|
479
|
+
2. **Implement a specific task** → \`${vars.bin} focus "<task name>"\`
|
|
480
|
+
User mentions a particular task by name. Examples: "implement the auth task", "work on caching"
|
|
481
|
+
|
|
482
|
+
3. **Capture a new task** → \`${vars.bin} new task\`
|
|
483
|
+
User has concrete work to add. Keywords: "task: ..." or "add a task ..."
|
|
484
|
+
|
|
485
|
+
4. **Capture a new goal** → \`${vars.bin} new goal\`
|
|
486
|
+
User has a higher-level objective to add. Keywords: "goal: ..." or "add a goal ..."
|
|
487
|
+
|
|
488
|
+
5. **Capture a vague idea** → \`${vars.bin} new idea\`
|
|
489
|
+
User has a rough idea that might become work later. Keywords: "idea: ..." or "add an idea ..."
|
|
490
|
+
|
|
491
|
+
6. **Unclear** → \`${vars.bin} help\`
|
|
492
|
+
If none of the above clearly apply, run this to see all available commands.
|
|
493
|
+
|
|
494
|
+
Do NOT proceed without running one of these commands.${instructions}
|
|
495
|
+
`;
|
|
496
|
+
}
|
|
339
497
|
async function agent(dependencies, env = process.env) {
|
|
340
498
|
const { context, fileSystem, settings } = dependencies;
|
|
341
499
|
if (env.DUST_SKIP_AGENT === "1") {
|
|
@@ -343,15 +501,13 @@ async function agent(dependencies, env = process.env) {
|
|
|
343
501
|
return { exitCode: 0 };
|
|
344
502
|
}
|
|
345
503
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
346
|
-
const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled);
|
|
347
|
-
context.stdout(
|
|
504
|
+
const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled, env);
|
|
505
|
+
context.stdout(agentGreeting(vars));
|
|
348
506
|
return { exitCode: 0 };
|
|
349
507
|
}
|
|
350
508
|
|
|
351
509
|
// lib/cli/commands/audit.ts
|
|
352
|
-
import {
|
|
353
|
-
import { basename, dirname as dirname2, join as join5 } from "node:path";
|
|
354
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
510
|
+
import { basename } from "node:path";
|
|
355
511
|
|
|
356
512
|
// lib/markdown/markdown-utilities.ts
|
|
357
513
|
function extractTitle(content) {
|
|
@@ -405,6 +561,286 @@ function extractOpeningSentence(content) {
|
|
|
405
561
|
return sentenceMatch[1];
|
|
406
562
|
}
|
|
407
563
|
|
|
564
|
+
// lib/audits/stock-audits.ts
|
|
565
|
+
function agentDeveloperExperience() {
|
|
566
|
+
return dedent`
|
|
567
|
+
# Agent Developer Experience
|
|
568
|
+
|
|
569
|
+
Review the codebase to ensure agents have everything they need to operate effectively. 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 you identify, avoiding duplication.
|
|
570
|
+
|
|
571
|
+
## Scope
|
|
572
|
+
|
|
573
|
+
Focus on these areas:
|
|
574
|
+
|
|
575
|
+
1. **Context window efficiency** - Are files small and well-organized?
|
|
576
|
+
2. **Test coverage** - Can agents verify correctness through tests?
|
|
577
|
+
3. **Feedback loop speed** - How fast are checks and tests?
|
|
578
|
+
4. **Debugging tools** - Can agents diagnose issues without trial and error?
|
|
579
|
+
5. **Structured logging** - Is system behavior observable through logs?
|
|
580
|
+
|
|
581
|
+
## Blocked By
|
|
582
|
+
|
|
583
|
+
(none)
|
|
584
|
+
|
|
585
|
+
## Definition of Done
|
|
586
|
+
|
|
587
|
+
- [ ] Reviewed file sizes and organization for context window fit
|
|
588
|
+
- [ ] Verified test coverage is sufficient for agent verification
|
|
589
|
+
- [ ] Measured feedback loop speed (time from change to check result)
|
|
590
|
+
- [ ] Confirmed debugging tools and structured logging are in place
|
|
591
|
+
- [ ] Proposed ideas for any improvements identified
|
|
592
|
+
`;
|
|
593
|
+
}
|
|
594
|
+
function deadCode() {
|
|
595
|
+
return dedent`
|
|
596
|
+
# Dead Code
|
|
597
|
+
|
|
598
|
+
Find and remove unused code to improve maintainability and reduce bundle size. 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 you identify, avoiding duplication.
|
|
599
|
+
|
|
600
|
+
## Scope
|
|
601
|
+
|
|
602
|
+
Focus on these areas:
|
|
603
|
+
|
|
604
|
+
1. **Unused exports** - Functions, classes, constants that are never imported
|
|
605
|
+
2. **Unreachable code** - Code after return statements, impossible conditions
|
|
606
|
+
3. **Orphaned files** - Files that are not imported anywhere
|
|
607
|
+
4. **Unused dependencies** - Packages in package.json not used in code
|
|
608
|
+
5. **Commented-out code** - Old code left in comments
|
|
609
|
+
|
|
610
|
+
## Blocked By
|
|
611
|
+
|
|
612
|
+
(none)
|
|
613
|
+
|
|
614
|
+
## Definition of Done
|
|
615
|
+
|
|
616
|
+
- [ ] Ran static analysis tools to find unused exports
|
|
617
|
+
- [ ] Identified files with no incoming imports
|
|
618
|
+
- [ ] Listed unused dependencies
|
|
619
|
+
- [ ] Reviewed commented-out code blocks
|
|
620
|
+
- [ ] Created list of code safe to remove
|
|
621
|
+
- [ ] Verified removal won't break dynamic imports or reflection
|
|
622
|
+
- [ ] Proposed ideas for any dead code worth removing
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
function factsVerification() {
|
|
626
|
+
return dedent`
|
|
627
|
+
# Facts Verification
|
|
628
|
+
|
|
629
|
+
Review \`.dust/facts/\` to ensure documented facts match current reality. 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 you identify, avoiding duplication.
|
|
630
|
+
|
|
631
|
+
## Scope
|
|
632
|
+
|
|
633
|
+
Focus on these areas:
|
|
634
|
+
|
|
635
|
+
1. **Accuracy** - Do documented facts reflect the current codebase?
|
|
636
|
+
2. **Completeness** - Are important implementation details documented?
|
|
637
|
+
3. **Staleness** - Have facts become outdated due to recent changes?
|
|
638
|
+
4. **Relevance** - Are all facts still useful for understanding the project?
|
|
639
|
+
|
|
640
|
+
## Blocked By
|
|
641
|
+
|
|
642
|
+
(none)
|
|
643
|
+
|
|
644
|
+
## Definition of Done
|
|
645
|
+
|
|
646
|
+
- [ ] Read each fact file in \`.dust/facts/\`
|
|
647
|
+
- [ ] Verified each fact against current codebase
|
|
648
|
+
- [ ] Identified outdated or inaccurate facts
|
|
649
|
+
- [ ] Listed missing facts that would help agents
|
|
650
|
+
- [ ] Updated or removed stale facts
|
|
651
|
+
- [ ] Proposed ideas for any facts improvements needed
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
function ideasFromCommits() {
|
|
655
|
+
return dedent`
|
|
656
|
+
# Ideas from Commits
|
|
657
|
+
|
|
658
|
+
Review recent commit history to identify follow-up 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.
|
|
659
|
+
|
|
660
|
+
## Scope
|
|
661
|
+
|
|
662
|
+
Focus on these areas:
|
|
663
|
+
|
|
664
|
+
1. **Technical debt** - Did recent work introduce shortcuts?
|
|
665
|
+
2. **Incomplete work** - Are there TODO comments or partial implementations?
|
|
666
|
+
3. **Pattern opportunities** - Can recent changes be generalized?
|
|
667
|
+
4. **Test gaps** - Do recent changes have adequate test coverage?
|
|
668
|
+
|
|
669
|
+
## Blocked By
|
|
670
|
+
|
|
671
|
+
(none)
|
|
672
|
+
|
|
673
|
+
## Definition of Done
|
|
674
|
+
|
|
675
|
+
- [ ] Reviewed commits from the last 20 commits
|
|
676
|
+
- [ ] Identified patterns or shortcuts worth addressing
|
|
677
|
+
- [ ] Listed TODO comments added in recent commits
|
|
678
|
+
- [ ] Noted areas where changes could be generalized
|
|
679
|
+
- [ ] Proposed follow-up ideas for any issues identified
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
function ideasFromGoals() {
|
|
683
|
+
return dedent`
|
|
684
|
+
# Ideas from Goals
|
|
685
|
+
|
|
686
|
+
Review \`.dust/goals/\` to generate new improvement ideas. Review existing ideas in \`./.ideas/\` and the recent history of \`./.dust/ideas\` to understand what has been proposed or considered historically, then create new idea files in \`./.ideas/\` for any issues or opportunities you identify, avoiding duplication.
|
|
687
|
+
|
|
688
|
+
## Scope
|
|
689
|
+
|
|
690
|
+
Focus on these areas:
|
|
691
|
+
|
|
692
|
+
1. **Unmet goals** - Which goals lack supporting work?
|
|
693
|
+
2. **Gap analysis** - Where does the codebase fall short of goals?
|
|
694
|
+
3. **New opportunities** - What work would better achieve each goal?
|
|
695
|
+
4. **Goal alignment** - Are current tasks aligned with stated goals?
|
|
696
|
+
|
|
697
|
+
## Blocked By
|
|
698
|
+
|
|
699
|
+
(none)
|
|
700
|
+
|
|
701
|
+
## Definition of Done
|
|
702
|
+
|
|
703
|
+
- [ ] Read each goal file in \`.dust/goals/\`
|
|
704
|
+
- [ ] Analyzed codebase for alignment with each goal
|
|
705
|
+
- [ ] Listed gaps between current state and goal intent
|
|
706
|
+
- [ ] Proposed new ideas for unmet or underserved goals
|
|
707
|
+
`;
|
|
708
|
+
}
|
|
709
|
+
function performanceReview() {
|
|
710
|
+
return dedent`
|
|
711
|
+
# Performance Review
|
|
712
|
+
|
|
713
|
+
Review the application for performance issues and optimization opportunities. 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 you identify, avoiding duplication.
|
|
714
|
+
|
|
715
|
+
## Scope
|
|
716
|
+
|
|
717
|
+
Focus on these areas:
|
|
718
|
+
|
|
719
|
+
1. **Startup time** - How fast does the application start?
|
|
720
|
+
2. **Command latency** - How responsive are CLI commands?
|
|
721
|
+
3. **Memory usage** - Is memory being used efficiently?
|
|
722
|
+
4. **Build performance** - How fast is the build process?
|
|
723
|
+
5. **Test speed** - Are tests running efficiently?
|
|
724
|
+
|
|
725
|
+
## Blocked By
|
|
726
|
+
|
|
727
|
+
(none)
|
|
728
|
+
|
|
729
|
+
## Definition of Done
|
|
730
|
+
|
|
731
|
+
- [ ] Measured startup time for common commands
|
|
732
|
+
- [ ] Profiled memory usage during typical operations
|
|
733
|
+
- [ ] Identified slow commands or operations
|
|
734
|
+
- [ ] Listed optimization opportunities by impact
|
|
735
|
+
- [ ] Proposed ideas for any performance improvements identified
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
function securityReview() {
|
|
739
|
+
return dedent`
|
|
740
|
+
# Security Review
|
|
741
|
+
|
|
742
|
+
Review the codebase for common security vulnerabilities and misconfigurations. 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 you identify, avoiding duplication.
|
|
743
|
+
|
|
744
|
+
## Scope
|
|
745
|
+
|
|
746
|
+
Focus on these areas:
|
|
747
|
+
|
|
748
|
+
1. **Hardcoded secrets** - API keys, passwords, tokens in source code
|
|
749
|
+
2. **Injection vulnerabilities** - SQL injection, command injection, XSS
|
|
750
|
+
3. **Authentication issues** - Weak password handling, missing auth checks
|
|
751
|
+
4. **Sensitive data exposure** - Logging sensitive data, insecure storage
|
|
752
|
+
5. **Dependency vulnerabilities** - Known CVEs in dependencies
|
|
753
|
+
|
|
754
|
+
## Blocked By
|
|
755
|
+
|
|
756
|
+
(none)
|
|
757
|
+
|
|
758
|
+
## Definition of Done
|
|
759
|
+
|
|
760
|
+
- [ ] Searched for hardcoded secrets (API keys, passwords, tokens)
|
|
761
|
+
- [ ] Reviewed input validation and sanitization
|
|
762
|
+
- [ ] Checked authentication and authorization logic
|
|
763
|
+
- [ ] Verified sensitive data is not logged or exposed
|
|
764
|
+
- [ ] Ran dependency audit for known vulnerabilities
|
|
765
|
+
- [ ] Documented any findings with severity ratings
|
|
766
|
+
- [ ] Proposed ideas for any security issues found
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
function staleIdeas() {
|
|
770
|
+
return dedent`
|
|
771
|
+
# Stale Ideas
|
|
772
|
+
|
|
773
|
+
Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant. 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 you identify, avoiding duplication.
|
|
774
|
+
|
|
775
|
+
## Scope
|
|
776
|
+
|
|
777
|
+
Focus on these areas:
|
|
778
|
+
|
|
779
|
+
1. **Age** - Ideas unchanged for many commits may need attention
|
|
780
|
+
2. **Relevance** - Has the project evolved past the idea?
|
|
781
|
+
3. **Actionability** - Can the idea be converted to a task?
|
|
782
|
+
4. **Duplication** - Are there overlapping or redundant ideas?
|
|
783
|
+
|
|
784
|
+
## Blocked By
|
|
785
|
+
|
|
786
|
+
(none)
|
|
787
|
+
|
|
788
|
+
## Definition of Done
|
|
789
|
+
|
|
790
|
+
- [ ] Listed all ideas with their last modification date
|
|
791
|
+
- [ ] Identified ideas unchanged for 50+ commits
|
|
792
|
+
- [ ] Reviewed each stale idea for current relevance
|
|
793
|
+
- [ ] Promoted actionable ideas to tasks
|
|
794
|
+
- [ ] Deleted ideas that are no longer relevant
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
function testCoverage() {
|
|
798
|
+
return dedent`
|
|
799
|
+
# Test Coverage
|
|
800
|
+
|
|
801
|
+
Identify untested code paths and areas that need additional test coverage. 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 you identify, avoiding duplication.
|
|
802
|
+
|
|
803
|
+
## Scope
|
|
804
|
+
|
|
805
|
+
Focus on these areas:
|
|
806
|
+
|
|
807
|
+
1. **Core business logic** - Functions that handle critical operations
|
|
808
|
+
2. **Edge cases** - Boundary conditions, error handling paths
|
|
809
|
+
3. **Integration points** - API endpoints, database operations
|
|
810
|
+
4. **User-facing features** - UI components, form validation
|
|
811
|
+
5. **Recent changes** - Code modified in the last few commits
|
|
812
|
+
|
|
813
|
+
## Blocked By
|
|
814
|
+
|
|
815
|
+
(none)
|
|
816
|
+
|
|
817
|
+
## Definition of Done
|
|
818
|
+
|
|
819
|
+
- [ ] Identified modules with low or no test coverage
|
|
820
|
+
- [ ] Listed critical paths that lack tests
|
|
821
|
+
- [ ] Prioritized areas by risk and importance
|
|
822
|
+
- [ ] Proposed ideas for any test coverage gaps identified
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
var stockAuditFunctions = {
|
|
826
|
+
"agent-developer-experience": agentDeveloperExperience,
|
|
827
|
+
"dead-code": deadCode,
|
|
828
|
+
"facts-verification": factsVerification,
|
|
829
|
+
"ideas-from-commits": ideasFromCommits,
|
|
830
|
+
"ideas-from-goals": ideasFromGoals,
|
|
831
|
+
"performance-review": performanceReview,
|
|
832
|
+
"security-review": securityReview,
|
|
833
|
+
"stale-ideas": staleIdeas,
|
|
834
|
+
"test-coverage": testCoverage
|
|
835
|
+
};
|
|
836
|
+
function loadStockAudits() {
|
|
837
|
+
return Object.entries(stockAuditFunctions).sort(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
|
|
838
|
+
const template = render();
|
|
839
|
+
const description = extractOpeningSentence(template);
|
|
840
|
+
return { name, description, template };
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
408
844
|
// lib/cli/colors.ts
|
|
409
845
|
var ANSI_COLORS = {
|
|
410
846
|
reset: "\x1B[0m",
|
|
@@ -439,17 +875,6 @@ function getColors() {
|
|
|
439
875
|
}
|
|
440
876
|
|
|
441
877
|
// lib/cli/commands/audit.ts
|
|
442
|
-
var __dirname3 = dirname2(fileURLToPath2(import.meta.url));
|
|
443
|
-
var stockAuditsDir = join5(__dirname3, "../../templates/audits");
|
|
444
|
-
function loadStockAudits() {
|
|
445
|
-
const files = readdirSync(stockAuditsDir).filter((f) => f.endsWith(".md")).sort();
|
|
446
|
-
return files.map((file) => {
|
|
447
|
-
const template = readFileSync2(join5(stockAuditsDir, file), "utf-8");
|
|
448
|
-
const name = basename(file, ".md");
|
|
449
|
-
const description = extractOpeningSentence(template);
|
|
450
|
-
return { name, description, template };
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
878
|
function transformAuditContent(content) {
|
|
454
879
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
455
880
|
if (!titleMatch) {
|
|
@@ -544,13 +969,13 @@ async function audit(dependencies) {
|
|
|
544
969
|
|
|
545
970
|
// lib/cli/commands/bucket.ts
|
|
546
971
|
import { spawn as nodeSpawn3 } from "node:child_process";
|
|
547
|
-
import { accessSync } from "node:fs";
|
|
972
|
+
import { accessSync, statSync } from "node:fs";
|
|
548
973
|
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
549
974
|
import { createServer as httpCreateServer } from "node:http";
|
|
550
975
|
import { homedir, tmpdir } from "node:os";
|
|
551
976
|
|
|
552
977
|
// lib/bucket/auth.ts
|
|
553
|
-
import { join as
|
|
978
|
+
import { join as join4 } from "node:path";
|
|
554
979
|
var CREDENTIALS_DIR = ".dust";
|
|
555
980
|
var CREDENTIALS_FILE = "credentials.json";
|
|
556
981
|
var AUTH_TIMEOUT_MS = 120000;
|
|
@@ -559,7 +984,7 @@ function getDustbucketHost() {
|
|
|
559
984
|
return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
|
|
560
985
|
}
|
|
561
986
|
function credentialsPath(homeDir) {
|
|
562
|
-
return
|
|
987
|
+
return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
|
|
563
988
|
}
|
|
564
989
|
async function loadStoredToken(fileSystem, homeDir) {
|
|
565
990
|
const path = credentialsPath(homeDir);
|
|
@@ -572,7 +997,7 @@ async function loadStoredToken(fileSystem, homeDir) {
|
|
|
572
997
|
}
|
|
573
998
|
}
|
|
574
999
|
async function storeToken(fileSystem, homeDir, token) {
|
|
575
|
-
const dirPath =
|
|
1000
|
+
const dirPath = join4(homeDir, CREDENTIALS_DIR);
|
|
576
1001
|
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
577
1002
|
await fileSystem.writeFile(credentialsPath(homeDir), JSON.stringify({ token }));
|
|
578
1003
|
}
|
|
@@ -694,7 +1119,7 @@ function getLogLines(buffer) {
|
|
|
694
1119
|
}
|
|
695
1120
|
|
|
696
1121
|
// lib/bucket/repository.ts
|
|
697
|
-
import { join as
|
|
1122
|
+
import { join as join6 } from "node:path";
|
|
698
1123
|
|
|
699
1124
|
// lib/agent-events.ts
|
|
700
1125
|
function rawEventToAgentEvent(rawEvent) {
|
|
@@ -1116,11 +1541,27 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
|
|
|
1116
1541
|
|
|
1117
1542
|
// lib/cli/commands/loop.ts
|
|
1118
1543
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
1544
|
+
import { readFileSync } from "node:fs";
|
|
1545
|
+
import os from "node:os";
|
|
1546
|
+
import { dirname, join as join5 } from "node:path";
|
|
1547
|
+
import { fileURLToPath } from "node:url";
|
|
1548
|
+
|
|
1549
|
+
// lib/workflow-tasks.ts
|
|
1550
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
1551
|
+
"Refine Idea: ",
|
|
1552
|
+
"Decompose Idea: ",
|
|
1553
|
+
"Shelve Idea: "
|
|
1554
|
+
];
|
|
1555
|
+
var BUILD_IDEA_PREFIX = "Build Idea: ";
|
|
1556
|
+
function titleToFilename(title) {
|
|
1557
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
1558
|
+
}
|
|
1119
1559
|
|
|
1120
1560
|
// lib/cli/commands/focus.ts
|
|
1121
1561
|
function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
1122
1562
|
const steps = [];
|
|
1123
1563
|
let step = 1;
|
|
1564
|
+
const hasIdeaFile = !taskTitle?.startsWith(BUILD_IDEA_PREFIX);
|
|
1124
1565
|
steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
|
|
1125
1566
|
steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
|
|
1126
1567
|
step++;
|
|
@@ -1131,7 +1572,15 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
|
1131
1572
|
step++;
|
|
1132
1573
|
}
|
|
1133
1574
|
const commitMessageLine = taskTitle ? ` Use this exact commit message: "${taskTitle}". Do not add any prefix.` : ' Use the task title as the commit message. Do not add prefixes like "Complete task:" - use the title directly.';
|
|
1134
|
-
|
|
1575
|
+
const commitItems = [
|
|
1576
|
+
" - All implementation changes",
|
|
1577
|
+
" - Deletion of the completed task file",
|
|
1578
|
+
" - Updates to any facts that changed"
|
|
1579
|
+
];
|
|
1580
|
+
if (hasIdeaFile) {
|
|
1581
|
+
commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
|
|
1582
|
+
}
|
|
1583
|
+
steps.push(`${step}. Create a single atomic commit that includes:`, ...commitItems, "", commitMessageLine, "");
|
|
1135
1584
|
step++;
|
|
1136
1585
|
steps.push(`${step}. Push your commit to the remote repository`);
|
|
1137
1586
|
steps.push("");
|
|
@@ -1235,6 +1684,29 @@ async function next(dependencies) {
|
|
|
1235
1684
|
}
|
|
1236
1685
|
|
|
1237
1686
|
// lib/cli/commands/loop.ts
|
|
1687
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
1688
|
+
function getDustVersion() {
|
|
1689
|
+
const candidates = [
|
|
1690
|
+
join5(__dirname2, "../../../package.json"),
|
|
1691
|
+
join5(__dirname2, "../package.json")
|
|
1692
|
+
];
|
|
1693
|
+
for (const candidate of candidates) {
|
|
1694
|
+
try {
|
|
1695
|
+
const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
1696
|
+
return packageJson.version ?? "unknown";
|
|
1697
|
+
} catch {}
|
|
1698
|
+
}
|
|
1699
|
+
return "unknown";
|
|
1700
|
+
}
|
|
1701
|
+
function getEnvironmentContext(cwd) {
|
|
1702
|
+
return {
|
|
1703
|
+
machineName: os.hostname(),
|
|
1704
|
+
cwd,
|
|
1705
|
+
platform: `${os.platform()} ${os.release()}`,
|
|
1706
|
+
dustVersion: getDustVersion(),
|
|
1707
|
+
runtimeVersion: process.version
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1238
1710
|
function formatLoopEvent(event) {
|
|
1239
1711
|
switch (event.type) {
|
|
1240
1712
|
case "loop.warning":
|
|
@@ -1351,7 +1823,8 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1351
1823
|
title: "Resolving git conflict",
|
|
1352
1824
|
prompt: prompt2,
|
|
1353
1825
|
agentType: "claude",
|
|
1354
|
-
purpose: "git-conflict"
|
|
1826
|
+
purpose: "git-conflict",
|
|
1827
|
+
...getEnvironmentContext(context.cwd)
|
|
1355
1828
|
});
|
|
1356
1829
|
try {
|
|
1357
1830
|
await run2(prompt2, {
|
|
@@ -1403,7 +1876,8 @@ ${instructions}`;
|
|
|
1403
1876
|
title: task.title ?? task.path,
|
|
1404
1877
|
prompt,
|
|
1405
1878
|
agentType: "claude",
|
|
1406
|
-
purpose: "task"
|
|
1879
|
+
purpose: "task",
|
|
1880
|
+
...getEnvironmentContext(context.cwd)
|
|
1407
1881
|
});
|
|
1408
1882
|
try {
|
|
1409
1883
|
await run2(prompt, {
|
|
@@ -1513,7 +1987,7 @@ function parseRepository(data) {
|
|
|
1513
1987
|
}
|
|
1514
1988
|
function getRepoTempPath(repoName, tempDir) {
|
|
1515
1989
|
const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
1516
|
-
return
|
|
1990
|
+
return join6(tempDir, `dust-bucket-${safeName}`);
|
|
1517
1991
|
}
|
|
1518
1992
|
async function cloneRepository(repository, targetPath, spawn, context) {
|
|
1519
1993
|
return new Promise((resolve) => {
|
|
@@ -1967,7 +2441,7 @@ function renderTabs(state) {
|
|
|
1967
2441
|
const width = name.length + 4;
|
|
1968
2442
|
if (i === state.selectedIndex) {
|
|
1969
2443
|
tabs.push({
|
|
1970
|
-
text:
|
|
2444
|
+
text: ` ${dot}${color} ${ANSI.INVERSE}${name}${ANSI.RESET} `,
|
|
1971
2445
|
width
|
|
1972
2446
|
});
|
|
1973
2447
|
} else {
|
|
@@ -2231,6 +2705,13 @@ function createDefaultBucketDependencies() {
|
|
|
2231
2705
|
return false;
|
|
2232
2706
|
}
|
|
2233
2707
|
},
|
|
2708
|
+
isDirectory: (path) => {
|
|
2709
|
+
try {
|
|
2710
|
+
return statSync(path).isDirectory();
|
|
2711
|
+
} catch {
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
},
|
|
2234
2715
|
readFile: (path) => readFile(path, "utf8"),
|
|
2235
2716
|
writeFile: (path, content) => writeFile(path, content, "utf8"),
|
|
2236
2717
|
mkdir: (path, options) => mkdir(path, options).then(() => {}),
|
|
@@ -2616,22 +3097,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
|
|
|
2616
3097
|
}
|
|
2617
3098
|
|
|
2618
3099
|
// lib/cli/commands/lint-markdown.ts
|
|
2619
|
-
import { dirname as
|
|
2620
|
-
|
|
2621
|
-
// lib/workflow-tasks.ts
|
|
2622
|
-
var IDEA_TRANSITION_PREFIXES = [
|
|
2623
|
-
"Refine Idea: ",
|
|
2624
|
-
"Decompose Idea: ",
|
|
2625
|
-
"Shelve Idea: "
|
|
2626
|
-
];
|
|
2627
|
-
function titleToFilename(title) {
|
|
2628
|
-
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
// lib/cli/commands/lint-markdown.ts
|
|
3100
|
+
import { dirname as dirname2, join as join7, resolve } from "node:path";
|
|
2632
3101
|
var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
|
|
2633
3102
|
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
2634
3103
|
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
3104
|
+
var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
2635
3105
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
2636
3106
|
function validateFilename(filePath) {
|
|
2637
3107
|
const parts = filePath.split("/");
|
|
@@ -2729,7 +3199,7 @@ function validateLinks(filePath, content, fileSystem) {
|
|
|
2729
3199
|
const violations = [];
|
|
2730
3200
|
const lines = content.split(`
|
|
2731
3201
|
`);
|
|
2732
|
-
const fileDir =
|
|
3202
|
+
const fileDir = dirname2(filePath);
|
|
2733
3203
|
for (let i = 0;i < lines.length; i++) {
|
|
2734
3204
|
const line = lines[i];
|
|
2735
3205
|
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
@@ -2846,7 +3316,7 @@ function validateSemanticLinks(filePath, content) {
|
|
|
2846
3316
|
const violations = [];
|
|
2847
3317
|
const lines = content.split(`
|
|
2848
3318
|
`);
|
|
2849
|
-
const fileDir =
|
|
3319
|
+
const fileDir = dirname2(filePath);
|
|
2850
3320
|
let currentSection = null;
|
|
2851
3321
|
for (let i = 0;i < lines.length; i++) {
|
|
2852
3322
|
const line = lines[i];
|
|
@@ -2929,7 +3399,7 @@ function validateGoalHierarchyLinks(filePath, content) {
|
|
|
2929
3399
|
const violations = [];
|
|
2930
3400
|
const lines = content.split(`
|
|
2931
3401
|
`);
|
|
2932
|
-
const fileDir =
|
|
3402
|
+
const fileDir = dirname2(filePath);
|
|
2933
3403
|
let currentSection = null;
|
|
2934
3404
|
for (let i = 0;i < lines.length; i++) {
|
|
2935
3405
|
const line = lines[i];
|
|
@@ -2979,7 +3449,7 @@ function validateGoalHierarchyLinks(filePath, content) {
|
|
|
2979
3449
|
function extractGoalRelationships(filePath, content) {
|
|
2980
3450
|
const lines = content.split(`
|
|
2981
3451
|
`);
|
|
2982
|
-
const fileDir =
|
|
3452
|
+
const fileDir = dirname2(filePath);
|
|
2983
3453
|
const parentGoals = [];
|
|
2984
3454
|
const subGoals = [];
|
|
2985
3455
|
let currentSection = null;
|
|
@@ -3085,6 +3555,72 @@ async function safeScanDir(glob, dirPath) {
|
|
|
3085
3555
|
throw error;
|
|
3086
3556
|
}
|
|
3087
3557
|
}
|
|
3558
|
+
async function validateContentDirectoryFiles(dirPath, fileSystem) {
|
|
3559
|
+
const violations = [];
|
|
3560
|
+
let entries;
|
|
3561
|
+
try {
|
|
3562
|
+
entries = await fileSystem.readdir(dirPath);
|
|
3563
|
+
} catch (error) {
|
|
3564
|
+
if (error.code === "ENOENT") {
|
|
3565
|
+
return [];
|
|
3566
|
+
}
|
|
3567
|
+
throw error;
|
|
3568
|
+
}
|
|
3569
|
+
for (const entry of entries) {
|
|
3570
|
+
const entryPath = `${dirPath}/${entry}`;
|
|
3571
|
+
if (entry.startsWith(".")) {
|
|
3572
|
+
violations.push({
|
|
3573
|
+
file: entryPath,
|
|
3574
|
+
message: `Hidden file "${entry}" found in content directory`
|
|
3575
|
+
});
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
if (fileSystem.isDirectory(entryPath)) {
|
|
3579
|
+
violations.push({
|
|
3580
|
+
file: entryPath,
|
|
3581
|
+
message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
|
|
3582
|
+
});
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
if (!entry.endsWith(".md")) {
|
|
3586
|
+
violations.push({
|
|
3587
|
+
file: entryPath,
|
|
3588
|
+
message: `Non-markdown file "${entry}" found in content directory`
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return violations;
|
|
3593
|
+
}
|
|
3594
|
+
async function validateDirectoryStructure(dustPath, fileSystem, extraDirectories = []) {
|
|
3595
|
+
const violations = [];
|
|
3596
|
+
let entries;
|
|
3597
|
+
try {
|
|
3598
|
+
entries = await fileSystem.readdir(dustPath);
|
|
3599
|
+
} catch (error) {
|
|
3600
|
+
if (error.code === "ENOENT") {
|
|
3601
|
+
return [];
|
|
3602
|
+
}
|
|
3603
|
+
throw error;
|
|
3604
|
+
}
|
|
3605
|
+
const allowedDirectories = new Set([
|
|
3606
|
+
...EXPECTED_DIRECTORIES,
|
|
3607
|
+
...extraDirectories
|
|
3608
|
+
]);
|
|
3609
|
+
for (const entry of entries) {
|
|
3610
|
+
const entryPath = `${dustPath}/${entry}`;
|
|
3611
|
+
if (!fileSystem.isDirectory(entryPath)) {
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
if (!allowedDirectories.has(entry)) {
|
|
3615
|
+
const allowedList = [...allowedDirectories].sort().join(", ");
|
|
3616
|
+
violations.push({
|
|
3617
|
+
file: entryPath,
|
|
3618
|
+
message: `Unexpected directory "${entry}" in .dust/. Allowed directories: ${allowedList}. To allow this directory, add it to "extraDirectories" in .dust/config/settings.json`
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
return violations;
|
|
3623
|
+
}
|
|
3088
3624
|
async function lintMarkdown(dependencies) {
|
|
3089
3625
|
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
3090
3626
|
const dustPath = `${context.cwd}/.dust`;
|
|
@@ -3096,6 +3632,26 @@ async function lintMarkdown(dependencies) {
|
|
|
3096
3632
|
}
|
|
3097
3633
|
const dustFiles = dustScan.files;
|
|
3098
3634
|
const violations = [];
|
|
3635
|
+
context.stdout("Validating directory structure...");
|
|
3636
|
+
violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
|
|
3637
|
+
const settingsPath = join7(dustPath, "config", "settings.json");
|
|
3638
|
+
if (fileSystem.exists(settingsPath)) {
|
|
3639
|
+
context.stdout("Validating settings.json...");
|
|
3640
|
+
try {
|
|
3641
|
+
const settingsContent = await fileSystem.readFile(settingsPath);
|
|
3642
|
+
const settingsViolations = validateSettingsJson(settingsContent);
|
|
3643
|
+
for (const sv of settingsViolations) {
|
|
3644
|
+
violations.push({
|
|
3645
|
+
file: settingsPath,
|
|
3646
|
+
message: sv.message
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
} catch (error) {
|
|
3650
|
+
if (error.code !== "ENOENT") {
|
|
3651
|
+
throw error;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3099
3655
|
context.stdout("Validating links in .dust/...");
|
|
3100
3656
|
for (const file of dustFiles) {
|
|
3101
3657
|
if (!file.endsWith(".md"))
|
|
@@ -3112,6 +3668,10 @@ async function lintMarkdown(dependencies) {
|
|
|
3112
3668
|
}
|
|
3113
3669
|
const contentDirs = ["goals", "facts", "ideas", "tasks"];
|
|
3114
3670
|
context.stdout("Validating content files...");
|
|
3671
|
+
for (const dir of contentDirs) {
|
|
3672
|
+
const dirPath = `${dustPath}/${dir}`;
|
|
3673
|
+
violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
|
|
3674
|
+
}
|
|
3115
3675
|
for (const dir of contentDirs) {
|
|
3116
3676
|
const dirPath = `${dustPath}/${dir}`;
|
|
3117
3677
|
const { files } = await safeScanDir(glob, dirPath);
|
|
@@ -3278,8 +3838,8 @@ async function runValidationCheck(dependencies) {
|
|
|
3278
3838
|
});
|
|
3279
3839
|
const durationMs = Date.now() - startTime;
|
|
3280
3840
|
return {
|
|
3281
|
-
name: "lint
|
|
3282
|
-
command: "dust lint
|
|
3841
|
+
name: "lint",
|
|
3842
|
+
command: "dust lint",
|
|
3283
3843
|
exitCode: result.exitCode,
|
|
3284
3844
|
output: outputLines.join(`
|
|
3285
3845
|
`),
|
|
@@ -3376,7 +3936,45 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
3376
3936
|
|
|
3377
3937
|
// lib/cli/commands/help.ts
|
|
3378
3938
|
function generateHelpText(settings) {
|
|
3379
|
-
|
|
3939
|
+
const bin = settings.dustCommand;
|
|
3940
|
+
return dedent`
|
|
3941
|
+
💨 dust - Flow state for AI coding agents.
|
|
3942
|
+
|
|
3943
|
+
Usage: ${bin} <command> [options]
|
|
3944
|
+
|
|
3945
|
+
Commands:
|
|
3946
|
+
init Initialize a new Dust repository
|
|
3947
|
+
lint Run lint checks on .dust/ files
|
|
3948
|
+
list List all items (tasks, ideas, goals, facts)
|
|
3949
|
+
tasks List tasks (actionable work with definitions of done)
|
|
3950
|
+
ideas List ideas (vague proposals, convert to tasks when ready)
|
|
3951
|
+
goals List goals (guiding principles, stable, rarely change)
|
|
3952
|
+
facts List facts (documentation of current system state)
|
|
3953
|
+
next Show tasks ready to work on (not blocked)
|
|
3954
|
+
check Run project-defined quality gate hook
|
|
3955
|
+
agent Agent greeting and routing instructions
|
|
3956
|
+
focus Declare current objective (for remote session tracking)
|
|
3957
|
+
pick task Pick the next task to work on
|
|
3958
|
+
implement task Implement a task
|
|
3959
|
+
new task Create a new task
|
|
3960
|
+
new goal Create a new goal
|
|
3961
|
+
new idea Create a new idea
|
|
3962
|
+
loop claude Run continuous Claude iteration on tasks
|
|
3963
|
+
pre push Git pre-push hook validation
|
|
3964
|
+
help Show this help message
|
|
3965
|
+
|
|
3966
|
+
🤖 Agent Guide
|
|
3967
|
+
|
|
3968
|
+
Dust is a lightweight planning system. The .dust/ directory contains:
|
|
3969
|
+
- goals/ - Guiding principles (stable, rarely change)
|
|
3970
|
+
- ideas/ - Proposals (convert to tasks when ready)
|
|
3971
|
+
- tasks/ - Actionable work with definitions of done
|
|
3972
|
+
- facts/ - Documentation of current system state
|
|
3973
|
+
|
|
3974
|
+
Workflow: Pick a task → implement it → delete the task file → commit atomically.
|
|
3975
|
+
|
|
3976
|
+
Run \`${bin} agent\` to get started!
|
|
3977
|
+
`;
|
|
3380
3978
|
}
|
|
3381
3979
|
async function help(dependencies) {
|
|
3382
3980
|
dependencies.context.stdout(generateHelpText(dependencies.settings));
|
|
@@ -3407,6 +4005,24 @@ var USE_DUST_FACT = `# Use dust for planning
|
|
|
3407
4005
|
|
|
3408
4006
|
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
3409
4007
|
`;
|
|
4008
|
+
function claudeMdContent(dustCommand) {
|
|
4009
|
+
return dedent`
|
|
4010
|
+
# Claude Code Instructions
|
|
4011
|
+
|
|
4012
|
+
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
4013
|
+
|
|
4014
|
+
Always immediately run \`${dustCommand} agent\` when you start working in this repository.
|
|
4015
|
+
`;
|
|
4016
|
+
}
|
|
4017
|
+
function agentsMdContent(dustCommand) {
|
|
4018
|
+
return dedent`
|
|
4019
|
+
# Agent Instructions
|
|
4020
|
+
|
|
4021
|
+
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
4022
|
+
|
|
4023
|
+
Always immediately run \`${dustCommand} agent\` when you start working in this repository.
|
|
4024
|
+
`;
|
|
4025
|
+
}
|
|
3410
4026
|
async function init(dependencies) {
|
|
3411
4027
|
const { context, fileSystem } = dependencies;
|
|
3412
4028
|
const colors = getColors();
|
|
@@ -3445,8 +4061,9 @@ async function init(dependencies) {
|
|
|
3445
4061
|
}
|
|
3446
4062
|
const claudeMdPath = `${context.cwd}/CLAUDE.md`;
|
|
3447
4063
|
try {
|
|
3448
|
-
|
|
3449
|
-
|
|
4064
|
+
await fileSystem.writeFile(claudeMdPath, claudeMdContent(dustCommand), {
|
|
4065
|
+
flag: "wx"
|
|
4066
|
+
});
|
|
3450
4067
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
|
|
3451
4068
|
} catch (error) {
|
|
3452
4069
|
if (error.code === "EEXIST") {
|
|
@@ -3457,8 +4074,9 @@ async function init(dependencies) {
|
|
|
3457
4074
|
}
|
|
3458
4075
|
const agentsMdPath = `${context.cwd}/AGENTS.md`;
|
|
3459
4076
|
try {
|
|
3460
|
-
|
|
3461
|
-
|
|
4077
|
+
await fileSystem.writeFile(agentsMdPath, agentsMdContent(dustCommand), {
|
|
4078
|
+
flag: "wx"
|
|
4079
|
+
});
|
|
3462
4080
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
|
|
3463
4081
|
} catch (error) {
|
|
3464
4082
|
if (error.code === "EEXIST") {
|
|
@@ -3605,23 +4223,147 @@ async function list(dependencies) {
|
|
|
3605
4223
|
return { exitCode: 0 };
|
|
3606
4224
|
}
|
|
3607
4225
|
|
|
3608
|
-
// lib/cli/
|
|
3609
|
-
|
|
4226
|
+
// lib/cli/commands/new-goal.ts
|
|
4227
|
+
function newGoalInstructions(vars) {
|
|
4228
|
+
const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
|
|
4229
|
+
return dedent`
|
|
4230
|
+
## Adding a New Goal
|
|
4231
|
+
|
|
4232
|
+
Goals are guiding principles that persist across tasks. They define the "why" behind the work.
|
|
4233
|
+
|
|
4234
|
+
${intro}
|
|
4235
|
+
1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
|
|
4236
|
+
2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
|
|
4237
|
+
3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
|
|
4238
|
+
4. Write a clear description explaining:
|
|
4239
|
+
- What this goal means in practice
|
|
4240
|
+
- Why it matters for the project
|
|
4241
|
+
- How to evaluate whether work supports this goal
|
|
4242
|
+
5. Run \`${vars.bin} lint\` to catch any formatting issues
|
|
4243
|
+
6. Create a single atomic commit with a message in the format "Add goal: <title>"
|
|
4244
|
+
7. Push your commit to the remote repository
|
|
4245
|
+
|
|
4246
|
+
Goals should be:
|
|
4247
|
+
- **Stable** - They rarely change once established
|
|
4248
|
+
- **Actionable** - Tasks can be linked to them
|
|
4249
|
+
- **Clear** - Anyone reading should understand what it means
|
|
4250
|
+
`;
|
|
4251
|
+
}
|
|
4252
|
+
async function newGoal(dependencies) {
|
|
3610
4253
|
const { context, settings } = dependencies;
|
|
3611
4254
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
3612
4255
|
const vars = templateVariables(settings, hooksInstalled);
|
|
3613
|
-
context.stdout(
|
|
4256
|
+
context.stdout(newGoalInstructions(vars));
|
|
3614
4257
|
return { exitCode: 0 };
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// lib/cli/commands/new-goal.ts
|
|
3618
|
-
var newGoal = createTemplateCommand("agent-new-goal");
|
|
4258
|
+
}
|
|
3619
4259
|
|
|
3620
4260
|
// lib/cli/commands/new-idea.ts
|
|
3621
|
-
|
|
4261
|
+
function newIdeaInstructions(vars) {
|
|
4262
|
+
return dedent`
|
|
4263
|
+
## Adding a New Idea
|
|
4264
|
+
|
|
4265
|
+
Follow these steps:
|
|
4266
|
+
|
|
4267
|
+
1. Run \`${vars.bin} ideas\` to see all existing ideas and avoid duplicates
|
|
4268
|
+
2. Create a new markdown file in \`.dust/ideas/\` with a descriptive kebab-case name (e.g., \`improve-error-messages.md\`)
|
|
4269
|
+
3. Add a title as the first line using an H1 heading (e.g., \`# Improve error messages\`)
|
|
4270
|
+
4. Write a brief description of the potential change or improvement
|
|
4271
|
+
5. If the idea has open questions, add an \`## Open Questions\` section (see below)
|
|
4272
|
+
6. Run \`${vars.bin} lint\` to catch any issues with the idea file format
|
|
4273
|
+
7. Create a single atomic commit with a message in the format "Add idea: <title>"
|
|
4274
|
+
8. Push your commit to the remote repository
|
|
4275
|
+
|
|
4276
|
+
### Open Questions section
|
|
4277
|
+
|
|
4278
|
+
Ideas exist to eventually spawn tasks, so they start intentionally vague. An optional \`## Open Questions\` section captures the decisions that need to be made before the idea becomes actionable. Each question is an h3 heading ending with \`?\`, and each option is an h4 heading with markdown content explaining the trade-offs:
|
|
4279
|
+
|
|
4280
|
+
\`\`\`markdown
|
|
4281
|
+
## Open Questions
|
|
4282
|
+
|
|
4283
|
+
### Should we take our own payments?
|
|
4284
|
+
|
|
4285
|
+
#### Yes, take our own payments
|
|
4286
|
+
|
|
4287
|
+
Lower costs and we become the seller of record, but requires a merchant account.
|
|
4288
|
+
|
|
4289
|
+
#### No, use a payment provider
|
|
4290
|
+
|
|
4291
|
+
Higher costs but simpler setup. No merchant account needed.
|
|
4292
|
+
|
|
4293
|
+
### Which storage backend should we use?
|
|
4294
|
+
|
|
4295
|
+
#### SQLite
|
|
4296
|
+
|
|
4297
|
+
Simple and embedded. Good for single-node deployments.
|
|
4298
|
+
|
|
4299
|
+
#### PostgreSQL
|
|
4300
|
+
|
|
4301
|
+
Scalable but requires a separate server.
|
|
4302
|
+
\`\`\`
|
|
4303
|
+
|
|
4304
|
+
Rules:
|
|
4305
|
+
- Questions are \`###\` headings and must end with \`?\`
|
|
4306
|
+
- Options are \`####\` headings beneath a question
|
|
4307
|
+
- Each question must have at least one option
|
|
4308
|
+
- Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
|
|
4309
|
+
`;
|
|
4310
|
+
}
|
|
4311
|
+
async function newIdea(dependencies) {
|
|
4312
|
+
const { context, settings } = dependencies;
|
|
4313
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
4314
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
4315
|
+
context.stdout(newIdeaInstructions(vars));
|
|
4316
|
+
return { exitCode: 0 };
|
|
4317
|
+
}
|
|
3622
4318
|
|
|
3623
4319
|
// lib/cli/commands/new-task.ts
|
|
3624
|
-
|
|
4320
|
+
function newTaskInstructions(vars) {
|
|
4321
|
+
const steps = [];
|
|
4322
|
+
steps.push("## Adding a New Task");
|
|
4323
|
+
steps.push("");
|
|
4324
|
+
if (vars.isClaudeCodeWeb) {
|
|
4325
|
+
steps.push("Follow these steps to create the task definition, then spawn a sub-agent for implementation.");
|
|
4326
|
+
steps.push("");
|
|
4327
|
+
steps.push("Use a todo list to track your progress through these steps.");
|
|
4328
|
+
} else {
|
|
4329
|
+
steps.push("Follow these steps:");
|
|
4330
|
+
}
|
|
4331
|
+
steps.push("");
|
|
4332
|
+
steps.push(`1. Run \`${vars.bin} ideas\` to see all existing ideas`);
|
|
4333
|
+
steps.push("2. Determine which ideas (if any) should be:");
|
|
4334
|
+
steps.push(" - **Deleted** - if the new task fully covers the idea");
|
|
4335
|
+
steps.push(" - **Updated** - if the idea's scope changes as a result of the task");
|
|
4336
|
+
steps.push("3. Research thoroughly to ensure the task will be clearly defined:");
|
|
4337
|
+
steps.push(" - Explore the codebase to understand existing patterns and relevant files");
|
|
4338
|
+
steps.push(" - Identify exactly which files need to change and how");
|
|
4339
|
+
steps.push(" - Resolve any ambiguities in the requirements before writing the task");
|
|
4340
|
+
steps.push(" - Gather specific technical details (function names, file paths, data structures)");
|
|
4341
|
+
steps.push(" - The goal is a task description with minimal ambiguity at implementation time");
|
|
4342
|
+
steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
|
|
4343
|
+
steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
|
|
4344
|
+
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.');
|
|
4345
|
+
steps.push("7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)");
|
|
4346
|
+
steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
|
|
4347
|
+
steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
|
|
4348
|
+
steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
|
|
4349
|
+
steps.push('11. Create a single atomic commit with a message in the format "Add task: <title>" that includes:');
|
|
4350
|
+
steps.push(" - The new task file");
|
|
4351
|
+
steps.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
|
|
4352
|
+
if (vars.isClaudeCodeWeb) {
|
|
4353
|
+
steps.push(`12. **Start a sub-agent** to implement the task: "Run \`${vars.bin} implement task\` and implement the task in \`.dust/tasks/[task-file].md\`"`);
|
|
4354
|
+
} else {
|
|
4355
|
+
steps.push("12. Push your commit to the remote repository");
|
|
4356
|
+
}
|
|
4357
|
+
return steps.join(`
|
|
4358
|
+
`);
|
|
4359
|
+
}
|
|
4360
|
+
async function newTask(dependencies) {
|
|
4361
|
+
const { context, settings } = dependencies;
|
|
4362
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
4363
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
4364
|
+
context.stdout(newTaskInstructions(vars));
|
|
4365
|
+
return { exitCode: 0 };
|
|
4366
|
+
}
|
|
3625
4367
|
|
|
3626
4368
|
// lib/cli/commands/pick-task.ts
|
|
3627
4369
|
async function pickTask(dependencies) {
|
|
@@ -3793,7 +4535,7 @@ async function facts(dependencies) {
|
|
|
3793
4535
|
// lib/cli/main.ts
|
|
3794
4536
|
var commandRegistry = {
|
|
3795
4537
|
init,
|
|
3796
|
-
|
|
4538
|
+
lint: lintMarkdown,
|
|
3797
4539
|
list,
|
|
3798
4540
|
tasks,
|
|
3799
4541
|
goals,
|
|
@@ -3862,6 +4604,13 @@ async function main(options) {
|
|
|
3862
4604
|
function createFileSystem(primitives) {
|
|
3863
4605
|
return {
|
|
3864
4606
|
exists: primitives.existsSync,
|
|
4607
|
+
isDirectory: (path) => {
|
|
4608
|
+
try {
|
|
4609
|
+
return primitives.statSync(path).isDirectory();
|
|
4610
|
+
} catch {
|
|
4611
|
+
return false;
|
|
4612
|
+
}
|
|
4613
|
+
},
|
|
3865
4614
|
readFile: (path) => primitives.readFile(path, "utf-8"),
|
|
3866
4615
|
writeFile: (path, content, options) => primitives.writeFile(path, content, {
|
|
3867
4616
|
encoding: "utf-8",
|
|
@@ -3901,4 +4650,4 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
|
|
|
3901
4650
|
}
|
|
3902
4651
|
|
|
3903
4652
|
// lib/cli/run.ts
|
|
3904
|
-
await wireEntry({ existsSync, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2 }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });
|
|
4653
|
+
await wireEntry({ existsSync, statSync: statSync2, readFile: readFile2, writeFile: writeFile2, mkdir: mkdir2, readdir: readdir2, chmod: chmod2 }, { argv: process.argv, cwd: () => process.cwd(), exit: process.exit }, { log: console.log, error: console.error });
|