@joshski/dust 0.1.42 → 0.1.44
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 +853 -102
- 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,14 @@ 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
|
-
import { homedir
|
|
975
|
+
import { homedir } from "node:os";
|
|
976
|
+
import { join as join7 } from "node:path";
|
|
551
977
|
|
|
552
978
|
// lib/bucket/auth.ts
|
|
553
|
-
import { join as
|
|
979
|
+
import { join as join4 } from "node:path";
|
|
554
980
|
var CREDENTIALS_DIR = ".dust";
|
|
555
981
|
var CREDENTIALS_FILE = "credentials.json";
|
|
556
982
|
var AUTH_TIMEOUT_MS = 120000;
|
|
@@ -559,7 +985,7 @@ function getDustbucketHost() {
|
|
|
559
985
|
return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
|
|
560
986
|
}
|
|
561
987
|
function credentialsPath(homeDir) {
|
|
562
|
-
return
|
|
988
|
+
return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
|
|
563
989
|
}
|
|
564
990
|
async function loadStoredToken(fileSystem, homeDir) {
|
|
565
991
|
const path = credentialsPath(homeDir);
|
|
@@ -572,7 +998,7 @@ async function loadStoredToken(fileSystem, homeDir) {
|
|
|
572
998
|
}
|
|
573
999
|
}
|
|
574
1000
|
async function storeToken(fileSystem, homeDir, token) {
|
|
575
|
-
const dirPath =
|
|
1001
|
+
const dirPath = join4(homeDir, CREDENTIALS_DIR);
|
|
576
1002
|
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
577
1003
|
await fileSystem.writeFile(credentialsPath(homeDir), JSON.stringify({ token }));
|
|
578
1004
|
}
|
|
@@ -694,7 +1120,7 @@ function getLogLines(buffer) {
|
|
|
694
1120
|
}
|
|
695
1121
|
|
|
696
1122
|
// lib/bucket/repository.ts
|
|
697
|
-
import { join as
|
|
1123
|
+
import { dirname as dirname2, join as join6 } from "node:path";
|
|
698
1124
|
|
|
699
1125
|
// lib/agent-events.ts
|
|
700
1126
|
function rawEventToAgentEvent(rawEvent) {
|
|
@@ -1116,11 +1542,27 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
|
|
|
1116
1542
|
|
|
1117
1543
|
// lib/cli/commands/loop.ts
|
|
1118
1544
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
1545
|
+
import { readFileSync } from "node:fs";
|
|
1546
|
+
import os from "node:os";
|
|
1547
|
+
import { dirname, join as join5 } from "node:path";
|
|
1548
|
+
import { fileURLToPath } from "node:url";
|
|
1549
|
+
|
|
1550
|
+
// lib/workflow-tasks.ts
|
|
1551
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
1552
|
+
"Refine Idea: ",
|
|
1553
|
+
"Decompose Idea: ",
|
|
1554
|
+
"Shelve Idea: "
|
|
1555
|
+
];
|
|
1556
|
+
var BUILD_IDEA_PREFIX = "Build Idea: ";
|
|
1557
|
+
function titleToFilename(title) {
|
|
1558
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
1559
|
+
}
|
|
1119
1560
|
|
|
1120
1561
|
// lib/cli/commands/focus.ts
|
|
1121
1562
|
function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
1122
1563
|
const steps = [];
|
|
1123
1564
|
let step = 1;
|
|
1565
|
+
const hasIdeaFile = !taskTitle?.startsWith(BUILD_IDEA_PREFIX);
|
|
1124
1566
|
steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
|
|
1125
1567
|
steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
|
|
1126
1568
|
step++;
|
|
@@ -1131,7 +1573,15 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
|
1131
1573
|
step++;
|
|
1132
1574
|
}
|
|
1133
1575
|
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
|
-
|
|
1576
|
+
const commitItems = [
|
|
1577
|
+
" - All implementation changes",
|
|
1578
|
+
" - Deletion of the completed task file",
|
|
1579
|
+
" - Updates to any facts that changed"
|
|
1580
|
+
];
|
|
1581
|
+
if (hasIdeaFile) {
|
|
1582
|
+
commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
|
|
1583
|
+
}
|
|
1584
|
+
steps.push(`${step}. Create a single atomic commit that includes:`, ...commitItems, "", commitMessageLine, "");
|
|
1135
1585
|
step++;
|
|
1136
1586
|
steps.push(`${step}. Push your commit to the remote repository`);
|
|
1137
1587
|
steps.push("");
|
|
@@ -1235,6 +1685,29 @@ async function next(dependencies) {
|
|
|
1235
1685
|
}
|
|
1236
1686
|
|
|
1237
1687
|
// lib/cli/commands/loop.ts
|
|
1688
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
1689
|
+
function getDustVersion() {
|
|
1690
|
+
const candidates = [
|
|
1691
|
+
join5(__dirname2, "../../../package.json"),
|
|
1692
|
+
join5(__dirname2, "../package.json")
|
|
1693
|
+
];
|
|
1694
|
+
for (const candidate of candidates) {
|
|
1695
|
+
try {
|
|
1696
|
+
const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
1697
|
+
return packageJson.version ?? "unknown";
|
|
1698
|
+
} catch {}
|
|
1699
|
+
}
|
|
1700
|
+
return "unknown";
|
|
1701
|
+
}
|
|
1702
|
+
function getEnvironmentContext(cwd) {
|
|
1703
|
+
return {
|
|
1704
|
+
machineName: os.hostname(),
|
|
1705
|
+
cwd,
|
|
1706
|
+
platform: `${os.platform()} ${os.release()}`,
|
|
1707
|
+
dustVersion: getDustVersion(),
|
|
1708
|
+
runtimeVersion: process.version
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1238
1711
|
function formatLoopEvent(event) {
|
|
1239
1712
|
switch (event.type) {
|
|
1240
1713
|
case "loop.warning":
|
|
@@ -1351,7 +1824,8 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1351
1824
|
title: "Resolving git conflict",
|
|
1352
1825
|
prompt: prompt2,
|
|
1353
1826
|
agentType: "claude",
|
|
1354
|
-
purpose: "git-conflict"
|
|
1827
|
+
purpose: "git-conflict",
|
|
1828
|
+
...getEnvironmentContext(context.cwd)
|
|
1355
1829
|
});
|
|
1356
1830
|
try {
|
|
1357
1831
|
await run2(prompt2, {
|
|
@@ -1403,7 +1877,8 @@ ${instructions}`;
|
|
|
1403
1877
|
title: task.title ?? task.path,
|
|
1404
1878
|
prompt,
|
|
1405
1879
|
agentType: "claude",
|
|
1406
|
-
purpose: "task"
|
|
1880
|
+
purpose: "task",
|
|
1881
|
+
...getEnvironmentContext(context.cwd)
|
|
1407
1882
|
});
|
|
1408
1883
|
try {
|
|
1409
1884
|
await run2(prompt, {
|
|
@@ -1511,9 +1986,9 @@ function parseRepository(data) {
|
|
|
1511
1986
|
}
|
|
1512
1987
|
return null;
|
|
1513
1988
|
}
|
|
1514
|
-
function
|
|
1515
|
-
const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
1516
|
-
return
|
|
1989
|
+
function getRepoPath(repoName, reposDir) {
|
|
1990
|
+
const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
|
|
1991
|
+
return join6(reposDir, safeName);
|
|
1517
1992
|
}
|
|
1518
1993
|
async function cloneRepository(repository, targetPath, spawn, context) {
|
|
1519
1994
|
return new Promise((resolve) => {
|
|
@@ -1654,7 +2129,8 @@ async function addRepository(repository, manager, repoDeps, context) {
|
|
|
1654
2129
|
if (manager.repositories.has(repository.name)) {
|
|
1655
2130
|
return;
|
|
1656
2131
|
}
|
|
1657
|
-
const repoPath =
|
|
2132
|
+
const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
|
|
2133
|
+
await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
|
|
1658
2134
|
if (repoDeps.fileSystem.exists(repoPath)) {
|
|
1659
2135
|
await removeRepository(repoPath, repoDeps.spawn, context);
|
|
1660
2136
|
}
|
|
@@ -1967,7 +2443,7 @@ function renderTabs(state) {
|
|
|
1967
2443
|
const width = name.length + 4;
|
|
1968
2444
|
if (i === state.selectedIndex) {
|
|
1969
2445
|
tabs.push({
|
|
1970
|
-
text:
|
|
2446
|
+
text: ` ${dot}${color} ${ANSI.INVERSE}${name}${ANSI.RESET} `,
|
|
1971
2447
|
width
|
|
1972
2448
|
});
|
|
1973
2449
|
} else {
|
|
@@ -2231,6 +2707,13 @@ function createDefaultBucketDependencies() {
|
|
|
2231
2707
|
return false;
|
|
2232
2708
|
}
|
|
2233
2709
|
},
|
|
2710
|
+
isDirectory: (path) => {
|
|
2711
|
+
try {
|
|
2712
|
+
return statSync(path).isDirectory();
|
|
2713
|
+
} catch {
|
|
2714
|
+
return false;
|
|
2715
|
+
}
|
|
2716
|
+
},
|
|
2234
2717
|
readFile: (path) => readFile(path, "utf8"),
|
|
2235
2718
|
writeFile: (path, content) => writeFile(path, content, "utf8"),
|
|
2236
2719
|
mkdir: (path, options) => mkdir(path, options).then(() => {}),
|
|
@@ -2247,7 +2730,7 @@ function createDefaultBucketDependencies() {
|
|
|
2247
2730
|
writeStdout: defaultWriteStdout,
|
|
2248
2731
|
isTTY: process.stdout.isTTY ?? false,
|
|
2249
2732
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
2250
|
-
|
|
2733
|
+
getReposDir: () => process.env.DUST_REPOS_DIR || join7(homedir(), ".dust", "repos"),
|
|
2251
2734
|
auth: {
|
|
2252
2735
|
createServer: defaultCreateServer,
|
|
2253
2736
|
openBrowser: defaultOpenBrowser,
|
|
@@ -2285,7 +2768,7 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
|
|
|
2285
2768
|
run,
|
|
2286
2769
|
fileSystem,
|
|
2287
2770
|
sleep: bucketDeps.sleep,
|
|
2288
|
-
|
|
2771
|
+
getReposDir: bucketDeps.getReposDir
|
|
2289
2772
|
};
|
|
2290
2773
|
}
|
|
2291
2774
|
function syncUIWithRepoList(state, repos) {
|
|
@@ -2616,22 +3099,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
|
|
|
2616
3099
|
}
|
|
2617
3100
|
|
|
2618
3101
|
// lib/cli/commands/lint-markdown.ts
|
|
2619
|
-
import { dirname as dirname3, resolve } from "node:path";
|
|
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
|
|
3102
|
+
import { dirname as dirname3, join as join8, resolve } from "node:path";
|
|
2632
3103
|
var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
|
|
2633
3104
|
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
2634
3105
|
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
3106
|
+
var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
2635
3107
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
2636
3108
|
function validateFilename(filePath) {
|
|
2637
3109
|
const parts = filePath.split("/");
|
|
@@ -3085,6 +3557,72 @@ async function safeScanDir(glob, dirPath) {
|
|
|
3085
3557
|
throw error;
|
|
3086
3558
|
}
|
|
3087
3559
|
}
|
|
3560
|
+
async function validateContentDirectoryFiles(dirPath, fileSystem) {
|
|
3561
|
+
const violations = [];
|
|
3562
|
+
let entries;
|
|
3563
|
+
try {
|
|
3564
|
+
entries = await fileSystem.readdir(dirPath);
|
|
3565
|
+
} catch (error) {
|
|
3566
|
+
if (error.code === "ENOENT") {
|
|
3567
|
+
return [];
|
|
3568
|
+
}
|
|
3569
|
+
throw error;
|
|
3570
|
+
}
|
|
3571
|
+
for (const entry of entries) {
|
|
3572
|
+
const entryPath = `${dirPath}/${entry}`;
|
|
3573
|
+
if (entry.startsWith(".")) {
|
|
3574
|
+
violations.push({
|
|
3575
|
+
file: entryPath,
|
|
3576
|
+
message: `Hidden file "${entry}" found in content directory`
|
|
3577
|
+
});
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3580
|
+
if (fileSystem.isDirectory(entryPath)) {
|
|
3581
|
+
violations.push({
|
|
3582
|
+
file: entryPath,
|
|
3583
|
+
message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
|
|
3584
|
+
});
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
if (!entry.endsWith(".md")) {
|
|
3588
|
+
violations.push({
|
|
3589
|
+
file: entryPath,
|
|
3590
|
+
message: `Non-markdown file "${entry}" found in content directory`
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
return violations;
|
|
3595
|
+
}
|
|
3596
|
+
async function validateDirectoryStructure(dustPath, fileSystem, extraDirectories = []) {
|
|
3597
|
+
const violations = [];
|
|
3598
|
+
let entries;
|
|
3599
|
+
try {
|
|
3600
|
+
entries = await fileSystem.readdir(dustPath);
|
|
3601
|
+
} catch (error) {
|
|
3602
|
+
if (error.code === "ENOENT") {
|
|
3603
|
+
return [];
|
|
3604
|
+
}
|
|
3605
|
+
throw error;
|
|
3606
|
+
}
|
|
3607
|
+
const allowedDirectories = new Set([
|
|
3608
|
+
...EXPECTED_DIRECTORIES,
|
|
3609
|
+
...extraDirectories
|
|
3610
|
+
]);
|
|
3611
|
+
for (const entry of entries) {
|
|
3612
|
+
const entryPath = `${dustPath}/${entry}`;
|
|
3613
|
+
if (!fileSystem.isDirectory(entryPath)) {
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
if (!allowedDirectories.has(entry)) {
|
|
3617
|
+
const allowedList = [...allowedDirectories].sort().join(", ");
|
|
3618
|
+
violations.push({
|
|
3619
|
+
file: entryPath,
|
|
3620
|
+
message: `Unexpected directory "${entry}" in .dust/. Allowed directories: ${allowedList}. To allow this directory, add it to "extraDirectories" in .dust/config/settings.json`
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
return violations;
|
|
3625
|
+
}
|
|
3088
3626
|
async function lintMarkdown(dependencies) {
|
|
3089
3627
|
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
3090
3628
|
const dustPath = `${context.cwd}/.dust`;
|
|
@@ -3096,6 +3634,26 @@ async function lintMarkdown(dependencies) {
|
|
|
3096
3634
|
}
|
|
3097
3635
|
const dustFiles = dustScan.files;
|
|
3098
3636
|
const violations = [];
|
|
3637
|
+
context.stdout("Validating directory structure...");
|
|
3638
|
+
violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
|
|
3639
|
+
const settingsPath = join8(dustPath, "config", "settings.json");
|
|
3640
|
+
if (fileSystem.exists(settingsPath)) {
|
|
3641
|
+
context.stdout("Validating settings.json...");
|
|
3642
|
+
try {
|
|
3643
|
+
const settingsContent = await fileSystem.readFile(settingsPath);
|
|
3644
|
+
const settingsViolations = validateSettingsJson(settingsContent);
|
|
3645
|
+
for (const sv of settingsViolations) {
|
|
3646
|
+
violations.push({
|
|
3647
|
+
file: settingsPath,
|
|
3648
|
+
message: sv.message
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
} catch (error) {
|
|
3652
|
+
if (error.code !== "ENOENT") {
|
|
3653
|
+
throw error;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3099
3657
|
context.stdout("Validating links in .dust/...");
|
|
3100
3658
|
for (const file of dustFiles) {
|
|
3101
3659
|
if (!file.endsWith(".md"))
|
|
@@ -3112,6 +3670,10 @@ async function lintMarkdown(dependencies) {
|
|
|
3112
3670
|
}
|
|
3113
3671
|
const contentDirs = ["goals", "facts", "ideas", "tasks"];
|
|
3114
3672
|
context.stdout("Validating content files...");
|
|
3673
|
+
for (const dir of contentDirs) {
|
|
3674
|
+
const dirPath = `${dustPath}/${dir}`;
|
|
3675
|
+
violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
|
|
3676
|
+
}
|
|
3115
3677
|
for (const dir of contentDirs) {
|
|
3116
3678
|
const dirPath = `${dustPath}/${dir}`;
|
|
3117
3679
|
const { files } = await safeScanDir(glob, dirPath);
|
|
@@ -3278,8 +3840,8 @@ async function runValidationCheck(dependencies) {
|
|
|
3278
3840
|
});
|
|
3279
3841
|
const durationMs = Date.now() - startTime;
|
|
3280
3842
|
return {
|
|
3281
|
-
name: "lint
|
|
3282
|
-
command: "dust lint
|
|
3843
|
+
name: "lint",
|
|
3844
|
+
command: "dust lint",
|
|
3283
3845
|
exitCode: result.exitCode,
|
|
3284
3846
|
output: outputLines.join(`
|
|
3285
3847
|
`),
|
|
@@ -3376,7 +3938,45 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
3376
3938
|
|
|
3377
3939
|
// lib/cli/commands/help.ts
|
|
3378
3940
|
function generateHelpText(settings) {
|
|
3379
|
-
|
|
3941
|
+
const bin = settings.dustCommand;
|
|
3942
|
+
return dedent`
|
|
3943
|
+
💨 dust - Flow state for AI coding agents.
|
|
3944
|
+
|
|
3945
|
+
Usage: ${bin} <command> [options]
|
|
3946
|
+
|
|
3947
|
+
Commands:
|
|
3948
|
+
init Initialize a new Dust repository
|
|
3949
|
+
lint Run lint checks on .dust/ files
|
|
3950
|
+
list List all items (tasks, ideas, goals, facts)
|
|
3951
|
+
tasks List tasks (actionable work with definitions of done)
|
|
3952
|
+
ideas List ideas (vague proposals, convert to tasks when ready)
|
|
3953
|
+
goals List goals (guiding principles, stable, rarely change)
|
|
3954
|
+
facts List facts (documentation of current system state)
|
|
3955
|
+
next Show tasks ready to work on (not blocked)
|
|
3956
|
+
check Run project-defined quality gate hook
|
|
3957
|
+
agent Agent greeting and routing instructions
|
|
3958
|
+
focus Declare current objective (for remote session tracking)
|
|
3959
|
+
pick task Pick the next task to work on
|
|
3960
|
+
implement task Implement a task
|
|
3961
|
+
new task Create a new task
|
|
3962
|
+
new goal Create a new goal
|
|
3963
|
+
new idea Create a new idea
|
|
3964
|
+
loop claude Run continuous Claude iteration on tasks
|
|
3965
|
+
pre push Git pre-push hook validation
|
|
3966
|
+
help Show this help message
|
|
3967
|
+
|
|
3968
|
+
🤖 Agent Guide
|
|
3969
|
+
|
|
3970
|
+
Dust is a lightweight planning system. The .dust/ directory contains:
|
|
3971
|
+
- goals/ - Guiding principles (stable, rarely change)
|
|
3972
|
+
- ideas/ - Proposals (convert to tasks when ready)
|
|
3973
|
+
- tasks/ - Actionable work with definitions of done
|
|
3974
|
+
- facts/ - Documentation of current system state
|
|
3975
|
+
|
|
3976
|
+
Workflow: Pick a task → implement it → delete the task file → commit atomically.
|
|
3977
|
+
|
|
3978
|
+
Run \`${bin} agent\` to get started!
|
|
3979
|
+
`;
|
|
3380
3980
|
}
|
|
3381
3981
|
async function help(dependencies) {
|
|
3382
3982
|
dependencies.context.stdout(generateHelpText(dependencies.settings));
|
|
@@ -3407,6 +4007,24 @@ var USE_DUST_FACT = `# Use dust for planning
|
|
|
3407
4007
|
|
|
3408
4008
|
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
3409
4009
|
`;
|
|
4010
|
+
function claudeMdContent(dustCommand) {
|
|
4011
|
+
return dedent`
|
|
4012
|
+
# Claude Code Instructions
|
|
4013
|
+
|
|
4014
|
+
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
4015
|
+
|
|
4016
|
+
Always immediately run \`${dustCommand} agent\` when you start working in this repository.
|
|
4017
|
+
`;
|
|
4018
|
+
}
|
|
4019
|
+
function agentsMdContent(dustCommand) {
|
|
4020
|
+
return dedent`
|
|
4021
|
+
# Agent Instructions
|
|
4022
|
+
|
|
4023
|
+
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
4024
|
+
|
|
4025
|
+
Always immediately run \`${dustCommand} agent\` when you start working in this repository.
|
|
4026
|
+
`;
|
|
4027
|
+
}
|
|
3410
4028
|
async function init(dependencies) {
|
|
3411
4029
|
const { context, fileSystem } = dependencies;
|
|
3412
4030
|
const colors = getColors();
|
|
@@ -3445,8 +4063,9 @@ async function init(dependencies) {
|
|
|
3445
4063
|
}
|
|
3446
4064
|
const claudeMdPath = `${context.cwd}/CLAUDE.md`;
|
|
3447
4065
|
try {
|
|
3448
|
-
|
|
3449
|
-
|
|
4066
|
+
await fileSystem.writeFile(claudeMdPath, claudeMdContent(dustCommand), {
|
|
4067
|
+
flag: "wx"
|
|
4068
|
+
});
|
|
3450
4069
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
|
|
3451
4070
|
} catch (error) {
|
|
3452
4071
|
if (error.code === "EEXIST") {
|
|
@@ -3457,8 +4076,9 @@ async function init(dependencies) {
|
|
|
3457
4076
|
}
|
|
3458
4077
|
const agentsMdPath = `${context.cwd}/AGENTS.md`;
|
|
3459
4078
|
try {
|
|
3460
|
-
|
|
3461
|
-
|
|
4079
|
+
await fileSystem.writeFile(agentsMdPath, agentsMdContent(dustCommand), {
|
|
4080
|
+
flag: "wx"
|
|
4081
|
+
});
|
|
3462
4082
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
|
|
3463
4083
|
} catch (error) {
|
|
3464
4084
|
if (error.code === "EEXIST") {
|
|
@@ -3605,23 +4225,147 @@ async function list(dependencies) {
|
|
|
3605
4225
|
return { exitCode: 0 };
|
|
3606
4226
|
}
|
|
3607
4227
|
|
|
3608
|
-
// lib/cli/
|
|
3609
|
-
|
|
4228
|
+
// lib/cli/commands/new-goal.ts
|
|
4229
|
+
function newGoalInstructions(vars) {
|
|
4230
|
+
const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
|
|
4231
|
+
return dedent`
|
|
4232
|
+
## Adding a New Goal
|
|
4233
|
+
|
|
4234
|
+
Goals are guiding principles that persist across tasks. They define the "why" behind the work.
|
|
4235
|
+
|
|
4236
|
+
${intro}
|
|
4237
|
+
1. Run \`${vars.bin} goals\` to see existing goals and avoid duplication
|
|
4238
|
+
2. Create a new markdown file in \`.dust/goals/\` with a descriptive kebab-case name (e.g., \`cross-platform-support.md\`)
|
|
4239
|
+
3. Add a title as the first line using an H1 heading (e.g., \`# Cross-platform support\`)
|
|
4240
|
+
4. Write a clear description explaining:
|
|
4241
|
+
- What this goal means in practice
|
|
4242
|
+
- Why it matters for the project
|
|
4243
|
+
- How to evaluate whether work supports this goal
|
|
4244
|
+
5. Run \`${vars.bin} lint\` to catch any formatting issues
|
|
4245
|
+
6. Create a single atomic commit with a message in the format "Add goal: <title>"
|
|
4246
|
+
7. Push your commit to the remote repository
|
|
4247
|
+
|
|
4248
|
+
Goals should be:
|
|
4249
|
+
- **Stable** - They rarely change once established
|
|
4250
|
+
- **Actionable** - Tasks can be linked to them
|
|
4251
|
+
- **Clear** - Anyone reading should understand what it means
|
|
4252
|
+
`;
|
|
4253
|
+
}
|
|
4254
|
+
async function newGoal(dependencies) {
|
|
3610
4255
|
const { context, settings } = dependencies;
|
|
3611
4256
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
3612
4257
|
const vars = templateVariables(settings, hooksInstalled);
|
|
3613
|
-
context.stdout(
|
|
4258
|
+
context.stdout(newGoalInstructions(vars));
|
|
3614
4259
|
return { exitCode: 0 };
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// lib/cli/commands/new-goal.ts
|
|
3618
|
-
var newGoal = createTemplateCommand("agent-new-goal");
|
|
4260
|
+
}
|
|
3619
4261
|
|
|
3620
4262
|
// lib/cli/commands/new-idea.ts
|
|
3621
|
-
|
|
4263
|
+
function newIdeaInstructions(vars) {
|
|
4264
|
+
return dedent`
|
|
4265
|
+
## Adding a New Idea
|
|
4266
|
+
|
|
4267
|
+
Follow these steps:
|
|
4268
|
+
|
|
4269
|
+
1. Run \`${vars.bin} ideas\` to see all existing ideas and avoid duplicates
|
|
4270
|
+
2. Create a new markdown file in \`.dust/ideas/\` with a descriptive kebab-case name (e.g., \`improve-error-messages.md\`)
|
|
4271
|
+
3. Add a title as the first line using an H1 heading (e.g., \`# Improve error messages\`)
|
|
4272
|
+
4. Write a brief description of the potential change or improvement
|
|
4273
|
+
5. If the idea has open questions, add an \`## Open Questions\` section (see below)
|
|
4274
|
+
6. Run \`${vars.bin} lint\` to catch any issues with the idea file format
|
|
4275
|
+
7. Create a single atomic commit with a message in the format "Add idea: <title>"
|
|
4276
|
+
8. Push your commit to the remote repository
|
|
4277
|
+
|
|
4278
|
+
### Open Questions section
|
|
4279
|
+
|
|
4280
|
+
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:
|
|
4281
|
+
|
|
4282
|
+
\`\`\`markdown
|
|
4283
|
+
## Open Questions
|
|
4284
|
+
|
|
4285
|
+
### Should we take our own payments?
|
|
4286
|
+
|
|
4287
|
+
#### Yes, take our own payments
|
|
4288
|
+
|
|
4289
|
+
Lower costs and we become the seller of record, but requires a merchant account.
|
|
4290
|
+
|
|
4291
|
+
#### No, use a payment provider
|
|
4292
|
+
|
|
4293
|
+
Higher costs but simpler setup. No merchant account needed.
|
|
4294
|
+
|
|
4295
|
+
### Which storage backend should we use?
|
|
4296
|
+
|
|
4297
|
+
#### SQLite
|
|
4298
|
+
|
|
4299
|
+
Simple and embedded. Good for single-node deployments.
|
|
4300
|
+
|
|
4301
|
+
#### PostgreSQL
|
|
4302
|
+
|
|
4303
|
+
Scalable but requires a separate server.
|
|
4304
|
+
\`\`\`
|
|
4305
|
+
|
|
4306
|
+
Rules:
|
|
4307
|
+
- Questions are \`###\` headings and must end with \`?\`
|
|
4308
|
+
- Options are \`####\` headings beneath a question
|
|
4309
|
+
- Each question must have at least one option
|
|
4310
|
+
- Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
|
|
4311
|
+
`;
|
|
4312
|
+
}
|
|
4313
|
+
async function newIdea(dependencies) {
|
|
4314
|
+
const { context, settings } = dependencies;
|
|
4315
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
4316
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
4317
|
+
context.stdout(newIdeaInstructions(vars));
|
|
4318
|
+
return { exitCode: 0 };
|
|
4319
|
+
}
|
|
3622
4320
|
|
|
3623
4321
|
// lib/cli/commands/new-task.ts
|
|
3624
|
-
|
|
4322
|
+
function newTaskInstructions(vars) {
|
|
4323
|
+
const steps = [];
|
|
4324
|
+
steps.push("## Adding a New Task");
|
|
4325
|
+
steps.push("");
|
|
4326
|
+
if (vars.isClaudeCodeWeb) {
|
|
4327
|
+
steps.push("Follow these steps to create the task definition, then spawn a sub-agent for implementation.");
|
|
4328
|
+
steps.push("");
|
|
4329
|
+
steps.push("Use a todo list to track your progress through these steps.");
|
|
4330
|
+
} else {
|
|
4331
|
+
steps.push("Follow these steps:");
|
|
4332
|
+
}
|
|
4333
|
+
steps.push("");
|
|
4334
|
+
steps.push(`1. Run \`${vars.bin} ideas\` to see all existing ideas`);
|
|
4335
|
+
steps.push("2. Determine which ideas (if any) should be:");
|
|
4336
|
+
steps.push(" - **Deleted** - if the new task fully covers the idea");
|
|
4337
|
+
steps.push(" - **Updated** - if the idea's scope changes as a result of the task");
|
|
4338
|
+
steps.push("3. Research thoroughly to ensure the task will be clearly defined:");
|
|
4339
|
+
steps.push(" - Explore the codebase to understand existing patterns and relevant files");
|
|
4340
|
+
steps.push(" - Identify exactly which files need to change and how");
|
|
4341
|
+
steps.push(" - Resolve any ambiguities in the requirements before writing the task");
|
|
4342
|
+
steps.push(" - Gather specific technical details (function names, file paths, data structures)");
|
|
4343
|
+
steps.push(" - The goal is a task description with minimal ambiguity at implementation time");
|
|
4344
|
+
steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
|
|
4345
|
+
steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
|
|
4346
|
+
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.');
|
|
4347
|
+
steps.push("7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)");
|
|
4348
|
+
steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
|
|
4349
|
+
steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
|
|
4350
|
+
steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
|
|
4351
|
+
steps.push('11. Create a single atomic commit with a message in the format "Add task: <title>" that includes:');
|
|
4352
|
+
steps.push(" - The new task file");
|
|
4353
|
+
steps.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
|
|
4354
|
+
if (vars.isClaudeCodeWeb) {
|
|
4355
|
+
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\`"`);
|
|
4356
|
+
} else {
|
|
4357
|
+
steps.push("12. Push your commit to the remote repository");
|
|
4358
|
+
}
|
|
4359
|
+
return steps.join(`
|
|
4360
|
+
`);
|
|
4361
|
+
}
|
|
4362
|
+
async function newTask(dependencies) {
|
|
4363
|
+
const { context, settings } = dependencies;
|
|
4364
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
4365
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
4366
|
+
context.stdout(newTaskInstructions(vars));
|
|
4367
|
+
return { exitCode: 0 };
|
|
4368
|
+
}
|
|
3625
4369
|
|
|
3626
4370
|
// lib/cli/commands/pick-task.ts
|
|
3627
4371
|
async function pickTask(dependencies) {
|
|
@@ -3793,7 +4537,7 @@ async function facts(dependencies) {
|
|
|
3793
4537
|
// lib/cli/main.ts
|
|
3794
4538
|
var commandRegistry = {
|
|
3795
4539
|
init,
|
|
3796
|
-
|
|
4540
|
+
lint: lintMarkdown,
|
|
3797
4541
|
list,
|
|
3798
4542
|
tasks,
|
|
3799
4543
|
goals,
|
|
@@ -3862,6 +4606,13 @@ async function main(options) {
|
|
|
3862
4606
|
function createFileSystem(primitives) {
|
|
3863
4607
|
return {
|
|
3864
4608
|
exists: primitives.existsSync,
|
|
4609
|
+
isDirectory: (path) => {
|
|
4610
|
+
try {
|
|
4611
|
+
return primitives.statSync(path).isDirectory();
|
|
4612
|
+
} catch {
|
|
4613
|
+
return false;
|
|
4614
|
+
}
|
|
4615
|
+
},
|
|
3865
4616
|
readFile: (path) => primitives.readFile(path, "utf-8"),
|
|
3866
4617
|
writeFile: (path, content, options) => primitives.writeFile(path, content, {
|
|
3867
4618
|
encoding: "utf-8",
|
|
@@ -3901,4 +4652,4 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
|
|
|
3901
4652
|
}
|
|
3902
4653
|
|
|
3903
4654
|
// 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 });
|
|
4655
|
+
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 });
|