@joshski/dust 0.1.41 → 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 +862 -111
- 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,12 +1541,28 @@ 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
|
-
function buildImplementationInstructions(bin, hooksInstalled) {
|
|
1561
|
+
function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
1122
1562
|
const steps = [];
|
|
1123
1563
|
let step = 1;
|
|
1124
|
-
|
|
1564
|
+
const hasIdeaFile = !taskTitle?.startsWith(BUILD_IDEA_PREFIX);
|
|
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++;
|
|
1127
1568
|
steps.push(`${step}. Implement the task`);
|
|
@@ -1130,7 +1571,16 @@ function buildImplementationInstructions(bin, hooksInstalled) {
|
|
|
1130
1571
|
steps.push(`${step}. Run \`${bin} check\` before committing`);
|
|
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.';
|
|
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, "");
|
|
1134
1584
|
step++;
|
|
1135
1585
|
steps.push(`${step}. Push your commit to the remote repository`);
|
|
1136
1586
|
steps.push("");
|
|
@@ -1234,6 +1684,29 @@ async function next(dependencies) {
|
|
|
1234
1684
|
}
|
|
1235
1685
|
|
|
1236
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
|
+
}
|
|
1237
1710
|
function formatLoopEvent(event) {
|
|
1238
1711
|
switch (event.type) {
|
|
1239
1712
|
case "loop.warning":
|
|
@@ -1333,7 +1806,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
|
|
|
1333
1806
|
type: "loop.sync_skipped",
|
|
1334
1807
|
reason: pullResult.message
|
|
1335
1808
|
});
|
|
1336
|
-
const prompt2 = `Note:
|
|
1809
|
+
const prompt2 = `Note: Do NOT run \`dust agent\`.
|
|
1337
1810
|
|
|
1338
1811
|
git pull failed with the following error:
|
|
1339
1812
|
|
|
@@ -1350,7 +1823,8 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1350
1823
|
title: "Resolving git conflict",
|
|
1351
1824
|
prompt: prompt2,
|
|
1352
1825
|
agentType: "claude",
|
|
1353
|
-
purpose: "git-conflict"
|
|
1826
|
+
purpose: "git-conflict",
|
|
1827
|
+
...getEnvironmentContext(context.cwd)
|
|
1354
1828
|
});
|
|
1355
1829
|
try {
|
|
1356
1830
|
await run2(prompt2, {
|
|
@@ -1383,14 +1857,14 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1383
1857
|
onLoopEvent({ type: "loop.tasks_found" });
|
|
1384
1858
|
const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
|
|
1385
1859
|
const { dustCommand, installCommand = "npm install" } = dependencies.settings;
|
|
1386
|
-
const instructions = buildImplementationInstructions(dustCommand, true);
|
|
1860
|
+
const instructions = buildImplementationInstructions(dustCommand, true, task.title ?? undefined);
|
|
1387
1861
|
const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
|
|
1388
1862
|
|
|
1389
|
-
## Task: ${task.title}
|
|
1390
|
-
|
|
1391
1863
|
The following is the contents of the task file \`${task.path}\`:
|
|
1392
1864
|
|
|
1865
|
+
----------
|
|
1393
1866
|
${taskContent}
|
|
1867
|
+
----------
|
|
1394
1868
|
|
|
1395
1869
|
When the task is complete, delete the task file \`${task.path}\`.
|
|
1396
1870
|
|
|
@@ -1402,7 +1876,8 @@ ${instructions}`;
|
|
|
1402
1876
|
title: task.title ?? task.path,
|
|
1403
1877
|
prompt,
|
|
1404
1878
|
agentType: "claude",
|
|
1405
|
-
purpose: "task"
|
|
1879
|
+
purpose: "task",
|
|
1880
|
+
...getEnvironmentContext(context.cwd)
|
|
1406
1881
|
});
|
|
1407
1882
|
try {
|
|
1408
1883
|
await run2(prompt, {
|
|
@@ -1512,7 +1987,7 @@ function parseRepository(data) {
|
|
|
1512
1987
|
}
|
|
1513
1988
|
function getRepoTempPath(repoName, tempDir) {
|
|
1514
1989
|
const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
1515
|
-
return
|
|
1990
|
+
return join6(tempDir, `dust-bucket-${safeName}`);
|
|
1516
1991
|
}
|
|
1517
1992
|
async function cloneRepository(repository, targetPath, spawn, context) {
|
|
1518
1993
|
return new Promise((resolve) => {
|
|
@@ -1966,7 +2441,7 @@ function renderTabs(state) {
|
|
|
1966
2441
|
const width = name.length + 4;
|
|
1967
2442
|
if (i === state.selectedIndex) {
|
|
1968
2443
|
tabs.push({
|
|
1969
|
-
text:
|
|
2444
|
+
text: ` ${dot}${color} ${ANSI.INVERSE}${name}${ANSI.RESET} `,
|
|
1970
2445
|
width
|
|
1971
2446
|
});
|
|
1972
2447
|
} else {
|
|
@@ -2230,6 +2705,13 @@ function createDefaultBucketDependencies() {
|
|
|
2230
2705
|
return false;
|
|
2231
2706
|
}
|
|
2232
2707
|
},
|
|
2708
|
+
isDirectory: (path) => {
|
|
2709
|
+
try {
|
|
2710
|
+
return statSync(path).isDirectory();
|
|
2711
|
+
} catch {
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
},
|
|
2233
2715
|
readFile: (path) => readFile(path, "utf8"),
|
|
2234
2716
|
writeFile: (path, content) => writeFile(path, content, "utf8"),
|
|
2235
2717
|
mkdir: (path, options) => mkdir(path, options).then(() => {}),
|
|
@@ -2478,9 +2960,10 @@ function createKeypressHandler(useTUI, state, onQuit, options) {
|
|
|
2478
2960
|
onQuit();
|
|
2479
2961
|
};
|
|
2480
2962
|
}
|
|
2481
|
-
async function resolveToken(
|
|
2482
|
-
|
|
2483
|
-
|
|
2963
|
+
async function resolveToken(authDeps, context) {
|
|
2964
|
+
const envToken = process.env.DUST_BUCKET_TOKEN;
|
|
2965
|
+
if (envToken) {
|
|
2966
|
+
return envToken;
|
|
2484
2967
|
}
|
|
2485
2968
|
const stored = await loadStoredToken(authDeps.fileSystem, authDeps.getHomeDir());
|
|
2486
2969
|
if (stored) {
|
|
@@ -2498,8 +2981,8 @@ async function resolveToken(commandArgs, authDeps, context) {
|
|
|
2498
2981
|
}
|
|
2499
2982
|
}
|
|
2500
2983
|
async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
|
|
2501
|
-
const {
|
|
2502
|
-
const token = await resolveToken(
|
|
2984
|
+
const { context, fileSystem } = dependencies;
|
|
2985
|
+
const token = await resolveToken(bucketDeps.auth, context);
|
|
2503
2986
|
if (!token) {
|
|
2504
2987
|
return { exitCode: 1 };
|
|
2505
2988
|
}
|
|
@@ -2614,22 +3097,11 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
|
|
|
2614
3097
|
}
|
|
2615
3098
|
|
|
2616
3099
|
// lib/cli/commands/lint-markdown.ts
|
|
2617
|
-
import { dirname as
|
|
2618
|
-
|
|
2619
|
-
// lib/workflow-tasks.ts
|
|
2620
|
-
var IDEA_TRANSITION_PREFIXES = [
|
|
2621
|
-
"Refine Idea: ",
|
|
2622
|
-
"Decompose Idea: ",
|
|
2623
|
-
"Shelve Idea: "
|
|
2624
|
-
];
|
|
2625
|
-
function titleToFilename(title) {
|
|
2626
|
-
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2627
|
-
}
|
|
2628
|
-
|
|
2629
|
-
// lib/cli/commands/lint-markdown.ts
|
|
3100
|
+
import { dirname as dirname2, join as join7, resolve } from "node:path";
|
|
2630
3101
|
var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
|
|
2631
3102
|
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
2632
3103
|
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
3104
|
+
var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
2633
3105
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
2634
3106
|
function validateFilename(filePath) {
|
|
2635
3107
|
const parts = filePath.split("/");
|
|
@@ -2727,7 +3199,7 @@ function validateLinks(filePath, content, fileSystem) {
|
|
|
2727
3199
|
const violations = [];
|
|
2728
3200
|
const lines = content.split(`
|
|
2729
3201
|
`);
|
|
2730
|
-
const fileDir =
|
|
3202
|
+
const fileDir = dirname2(filePath);
|
|
2731
3203
|
for (let i = 0;i < lines.length; i++) {
|
|
2732
3204
|
const line = lines[i];
|
|
2733
3205
|
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
@@ -2844,7 +3316,7 @@ function validateSemanticLinks(filePath, content) {
|
|
|
2844
3316
|
const violations = [];
|
|
2845
3317
|
const lines = content.split(`
|
|
2846
3318
|
`);
|
|
2847
|
-
const fileDir =
|
|
3319
|
+
const fileDir = dirname2(filePath);
|
|
2848
3320
|
let currentSection = null;
|
|
2849
3321
|
for (let i = 0;i < lines.length; i++) {
|
|
2850
3322
|
const line = lines[i];
|
|
@@ -2927,7 +3399,7 @@ function validateGoalHierarchyLinks(filePath, content) {
|
|
|
2927
3399
|
const violations = [];
|
|
2928
3400
|
const lines = content.split(`
|
|
2929
3401
|
`);
|
|
2930
|
-
const fileDir =
|
|
3402
|
+
const fileDir = dirname2(filePath);
|
|
2931
3403
|
let currentSection = null;
|
|
2932
3404
|
for (let i = 0;i < lines.length; i++) {
|
|
2933
3405
|
const line = lines[i];
|
|
@@ -2977,7 +3449,7 @@ function validateGoalHierarchyLinks(filePath, content) {
|
|
|
2977
3449
|
function extractGoalRelationships(filePath, content) {
|
|
2978
3450
|
const lines = content.split(`
|
|
2979
3451
|
`);
|
|
2980
|
-
const fileDir =
|
|
3452
|
+
const fileDir = dirname2(filePath);
|
|
2981
3453
|
const parentGoals = [];
|
|
2982
3454
|
const subGoals = [];
|
|
2983
3455
|
let currentSection = null;
|
|
@@ -3083,6 +3555,72 @@ async function safeScanDir(glob, dirPath) {
|
|
|
3083
3555
|
throw error;
|
|
3084
3556
|
}
|
|
3085
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
|
+
}
|
|
3086
3624
|
async function lintMarkdown(dependencies) {
|
|
3087
3625
|
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
3088
3626
|
const dustPath = `${context.cwd}/.dust`;
|
|
@@ -3094,6 +3632,26 @@ async function lintMarkdown(dependencies) {
|
|
|
3094
3632
|
}
|
|
3095
3633
|
const dustFiles = dustScan.files;
|
|
3096
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
|
+
}
|
|
3097
3655
|
context.stdout("Validating links in .dust/...");
|
|
3098
3656
|
for (const file of dustFiles) {
|
|
3099
3657
|
if (!file.endsWith(".md"))
|
|
@@ -3110,6 +3668,10 @@ async function lintMarkdown(dependencies) {
|
|
|
3110
3668
|
}
|
|
3111
3669
|
const contentDirs = ["goals", "facts", "ideas", "tasks"];
|
|
3112
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
|
+
}
|
|
3113
3675
|
for (const dir of contentDirs) {
|
|
3114
3676
|
const dirPath = `${dustPath}/${dir}`;
|
|
3115
3677
|
const { files } = await safeScanDir(glob, dirPath);
|
|
@@ -3276,8 +3838,8 @@ async function runValidationCheck(dependencies) {
|
|
|
3276
3838
|
});
|
|
3277
3839
|
const durationMs = Date.now() - startTime;
|
|
3278
3840
|
return {
|
|
3279
|
-
name: "lint
|
|
3280
|
-
command: "dust lint
|
|
3841
|
+
name: "lint",
|
|
3842
|
+
command: "dust lint",
|
|
3281
3843
|
exitCode: result.exitCode,
|
|
3282
3844
|
output: outputLines.join(`
|
|
3283
3845
|
`),
|
|
@@ -3374,7 +3936,45 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
3374
3936
|
|
|
3375
3937
|
// lib/cli/commands/help.ts
|
|
3376
3938
|
function generateHelpText(settings) {
|
|
3377
|
-
|
|
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
|
+
`;
|
|
3378
3978
|
}
|
|
3379
3979
|
async function help(dependencies) {
|
|
3380
3980
|
dependencies.context.stdout(generateHelpText(dependencies.settings));
|
|
@@ -3405,6 +4005,24 @@ var USE_DUST_FACT = `# Use dust for planning
|
|
|
3405
4005
|
|
|
3406
4006
|
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
3407
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
|
+
}
|
|
3408
4026
|
async function init(dependencies) {
|
|
3409
4027
|
const { context, fileSystem } = dependencies;
|
|
3410
4028
|
const colors = getColors();
|
|
@@ -3443,8 +4061,9 @@ async function init(dependencies) {
|
|
|
3443
4061
|
}
|
|
3444
4062
|
const claudeMdPath = `${context.cwd}/CLAUDE.md`;
|
|
3445
4063
|
try {
|
|
3446
|
-
|
|
3447
|
-
|
|
4064
|
+
await fileSystem.writeFile(claudeMdPath, claudeMdContent(dustCommand), {
|
|
4065
|
+
flag: "wx"
|
|
4066
|
+
});
|
|
3448
4067
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
|
|
3449
4068
|
} catch (error) {
|
|
3450
4069
|
if (error.code === "EEXIST") {
|
|
@@ -3455,8 +4074,9 @@ async function init(dependencies) {
|
|
|
3455
4074
|
}
|
|
3456
4075
|
const agentsMdPath = `${context.cwd}/AGENTS.md`;
|
|
3457
4076
|
try {
|
|
3458
|
-
|
|
3459
|
-
|
|
4077
|
+
await fileSystem.writeFile(agentsMdPath, agentsMdContent(dustCommand), {
|
|
4078
|
+
flag: "wx"
|
|
4079
|
+
});
|
|
3460
4080
|
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
|
|
3461
4081
|
} catch (error) {
|
|
3462
4082
|
if (error.code === "EEXIST") {
|
|
@@ -3603,23 +4223,147 @@ async function list(dependencies) {
|
|
|
3603
4223
|
return { exitCode: 0 };
|
|
3604
4224
|
}
|
|
3605
4225
|
|
|
3606
|
-
// lib/cli/
|
|
3607
|
-
|
|
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) {
|
|
3608
4253
|
const { context, settings } = dependencies;
|
|
3609
4254
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
3610
4255
|
const vars = templateVariables(settings, hooksInstalled);
|
|
3611
|
-
context.stdout(
|
|
4256
|
+
context.stdout(newGoalInstructions(vars));
|
|
3612
4257
|
return { exitCode: 0 };
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
|
-
// lib/cli/commands/new-goal.ts
|
|
3616
|
-
var newGoal = createTemplateCommand("agent-new-goal");
|
|
4258
|
+
}
|
|
3617
4259
|
|
|
3618
4260
|
// lib/cli/commands/new-idea.ts
|
|
3619
|
-
|
|
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
|
+
}
|
|
3620
4318
|
|
|
3621
4319
|
// lib/cli/commands/new-task.ts
|
|
3622
|
-
|
|
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
|
+
}
|
|
3623
4367
|
|
|
3624
4368
|
// lib/cli/commands/pick-task.ts
|
|
3625
4369
|
async function pickTask(dependencies) {
|
|
@@ -3791,7 +4535,7 @@ async function facts(dependencies) {
|
|
|
3791
4535
|
// lib/cli/main.ts
|
|
3792
4536
|
var commandRegistry = {
|
|
3793
4537
|
init,
|
|
3794
|
-
|
|
4538
|
+
lint: lintMarkdown,
|
|
3795
4539
|
list,
|
|
3796
4540
|
tasks,
|
|
3797
4541
|
goals,
|
|
@@ -3860,6 +4604,13 @@ async function main(options) {
|
|
|
3860
4604
|
function createFileSystem(primitives) {
|
|
3861
4605
|
return {
|
|
3862
4606
|
exists: primitives.existsSync,
|
|
4607
|
+
isDirectory: (path) => {
|
|
4608
|
+
try {
|
|
4609
|
+
return primitives.statSync(path).isDirectory();
|
|
4610
|
+
} catch {
|
|
4611
|
+
return false;
|
|
4612
|
+
}
|
|
4613
|
+
},
|
|
3863
4614
|
readFile: (path) => primitives.readFile(path, "utf-8"),
|
|
3864
4615
|
writeFile: (path, content, options) => primitives.writeFile(path, content, {
|
|
3865
4616
|
encoding: "utf-8",
|
|
@@ -3899,4 +4650,4 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
|
|
|
3899
4650
|
}
|
|
3900
4651
|
|
|
3901
4652
|
// lib/cli/run.ts
|
|
3902
|
-
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 });
|