@joshski/dust 0.1.21 → 0.1.23
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/artifacts.js +59 -0
- package/dist/dust.js +247 -102
- package/package.json +5 -2
- package/templates/agent-greeting.txt +6 -0
- package/templates/agent-new-idea.txt +38 -3
- package/templates/agent-new-task.txt +1 -1
- package/templates/help.txt +1 -1
- package/templates/templates/agent-greeting.txt +6 -0
- package/templates/templates/agent-new-idea.txt +38 -3
- package/templates/templates/agent-new-task.txt +1 -1
- package/templates/templates/help.txt +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// lib/artifacts/idea-transition-task.ts
|
|
2
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
3
|
+
"Refine Idea: ",
|
|
4
|
+
"Create Task From Idea: ",
|
|
5
|
+
"Shelve Idea: "
|
|
6
|
+
];
|
|
7
|
+
var TRANSITION_PREFIX_MAP = {
|
|
8
|
+
"refine-idea": "Refine Idea: ",
|
|
9
|
+
"create-task-from-idea": "Create Task From Idea: ",
|
|
10
|
+
"shelve-idea": "Shelve Idea: "
|
|
11
|
+
};
|
|
12
|
+
function titleToFilename(title) {
|
|
13
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
14
|
+
}
|
|
15
|
+
async function createIdeaTransitionTask(fileSystem, dustPath, input) {
|
|
16
|
+
const ideaPath = `${dustPath}/ideas/${input.ideaSlug}.md`;
|
|
17
|
+
if (!fileSystem.exists(ideaPath)) {
|
|
18
|
+
throw new Error(`Idea not found: "${input.ideaSlug}" (expected file at ${ideaPath})`);
|
|
19
|
+
}
|
|
20
|
+
const ideaContent = await fileSystem.readFile(ideaPath);
|
|
21
|
+
const ideaTitleMatch = ideaContent.match(/^#\s+(.+)$/m);
|
|
22
|
+
if (!ideaTitleMatch) {
|
|
23
|
+
throw new Error(`Idea file has no title: ${ideaPath}`);
|
|
24
|
+
}
|
|
25
|
+
const ideaTitle = ideaTitleMatch[1].trim();
|
|
26
|
+
const prefix = TRANSITION_PREFIX_MAP[input.transition];
|
|
27
|
+
const taskTitle = `${prefix}${ideaTitle}`;
|
|
28
|
+
const filename = titleToFilename(taskTitle);
|
|
29
|
+
const filePath = `${dustPath}/tasks/${filename}`;
|
|
30
|
+
const goalsSection = input.goals.length > 0 ? input.goals.map((slug) => `- [${slug}](../goals/${slug}.md)`).join(`
|
|
31
|
+
`) : "(none)";
|
|
32
|
+
const blockedBySection = input.blockedBy.length > 0 ? input.blockedBy.map((slug) => `- [${slug}](../tasks/${slug}.md)`).join(`
|
|
33
|
+
`) : "(none)";
|
|
34
|
+
const definitionOfDoneSection = input.definitionOfDone.map((item) => `- [ ] ${item}`).join(`
|
|
35
|
+
`);
|
|
36
|
+
const content = `# ${taskTitle}
|
|
37
|
+
|
|
38
|
+
${input.openingSentence}
|
|
39
|
+
|
|
40
|
+
## Goals
|
|
41
|
+
|
|
42
|
+
${goalsSection}
|
|
43
|
+
|
|
44
|
+
## Blocked By
|
|
45
|
+
|
|
46
|
+
${blockedBySection}
|
|
47
|
+
|
|
48
|
+
## Definition of Done
|
|
49
|
+
|
|
50
|
+
${definitionOfDoneSection}
|
|
51
|
+
`;
|
|
52
|
+
await fileSystem.writeFile(filePath, content);
|
|
53
|
+
return { filePath };
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
titleToFilename,
|
|
57
|
+
createIdeaTransitionTask,
|
|
58
|
+
IDEA_TRANSITION_PREFIXES
|
|
59
|
+
};
|
package/dist/dust.js
CHANGED
|
@@ -108,6 +108,9 @@ function loadTemplate(name, variables = {}) {
|
|
|
108
108
|
return content;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// lib/cli/commands/agent-shared.ts
|
|
112
|
+
import { join as join4 } from "node:path";
|
|
113
|
+
|
|
111
114
|
// lib/agents/detection.ts
|
|
112
115
|
function detectAgent(env = process.env) {
|
|
113
116
|
if (env.CLAUDECODE) {
|
|
@@ -252,6 +255,18 @@ ${newHookContent}
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
// lib/cli/commands/agent-shared.ts
|
|
258
|
+
async function loadAgentInstructions(cwd, fileSystem, agentType) {
|
|
259
|
+
const instructionsPath = join4(cwd, ".dust", "config", "agents", `${agentType}.md`);
|
|
260
|
+
if (!fileSystem.exists(instructionsPath)) {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const content = await fileSystem.readFile(instructionsPath);
|
|
265
|
+
return content.trim();
|
|
266
|
+
} catch {
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
255
270
|
function templateVariables(settings, hooksInstalled, env = process.env) {
|
|
256
271
|
const agent = detectAgent(env);
|
|
257
272
|
return {
|
|
@@ -261,6 +276,17 @@ function templateVariables(settings, hooksInstalled, env = process.env) {
|
|
|
261
276
|
isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : ""
|
|
262
277
|
};
|
|
263
278
|
}
|
|
279
|
+
async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env) {
|
|
280
|
+
const agent = detectAgent(env);
|
|
281
|
+
const agentInstructions = await loadAgentInstructions(cwd, fileSystem, agent.type);
|
|
282
|
+
return {
|
|
283
|
+
bin: settings.dustCommand,
|
|
284
|
+
agentName: agent.name,
|
|
285
|
+
hooksInstalled: hooksInstalled ? "true" : "false",
|
|
286
|
+
isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : "",
|
|
287
|
+
agentInstructions
|
|
288
|
+
};
|
|
289
|
+
}
|
|
264
290
|
async function manageGitHooks(dependencies) {
|
|
265
291
|
const { context, fileSystem, settings } = dependencies;
|
|
266
292
|
const hooks = createHooksManager(context.cwd, fileSystem, settings);
|
|
@@ -281,19 +307,59 @@ async function manageGitHooks(dependencies) {
|
|
|
281
307
|
|
|
282
308
|
// lib/cli/commands/agent.ts
|
|
283
309
|
async function agent(dependencies) {
|
|
284
|
-
const { context, settings } = dependencies;
|
|
310
|
+
const { context, fileSystem, settings } = dependencies;
|
|
285
311
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
286
|
-
const vars =
|
|
312
|
+
const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled);
|
|
287
313
|
context.stdout(loadTemplate("agent-greeting", vars));
|
|
288
314
|
return { exitCode: 0 };
|
|
289
315
|
}
|
|
290
316
|
|
|
291
|
-
// lib/cli/
|
|
317
|
+
// lib/cli/process-runner.ts
|
|
292
318
|
import { spawn } from "node:child_process";
|
|
319
|
+
function createShellRunner(spawnFn) {
|
|
320
|
+
return {
|
|
321
|
+
run: (command, cwd) => runBufferedProcess(spawnFn, command, [], cwd, true)
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
var defaultShellRunner = createShellRunner(spawn);
|
|
325
|
+
function createGitRunner(spawnFn) {
|
|
326
|
+
return {
|
|
327
|
+
run: (gitArguments, cwd) => runBufferedProcess(spawnFn, "git", gitArguments, cwd, false)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
var defaultGitRunner = createGitRunner(spawn);
|
|
331
|
+
function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell) {
|
|
332
|
+
return new Promise((resolve) => {
|
|
333
|
+
const proc = spawnFn(command, commandArguments, { cwd, shell });
|
|
334
|
+
const chunks = [];
|
|
335
|
+
proc.stdout?.on("data", (data) => {
|
|
336
|
+
chunks.push(data.toString());
|
|
337
|
+
});
|
|
338
|
+
proc.stderr?.on("data", (data) => {
|
|
339
|
+
chunks.push(data.toString());
|
|
340
|
+
});
|
|
341
|
+
proc.on("close", (code) => {
|
|
342
|
+
resolve({ exitCode: code ?? 1, output: chunks.join("") });
|
|
343
|
+
});
|
|
344
|
+
proc.on("error", (error) => {
|
|
345
|
+
resolve({ exitCode: 1, output: error.message });
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
293
349
|
|
|
294
350
|
// lib/cli/commands/lint-markdown.ts
|
|
295
351
|
import { dirname as dirname2, resolve } from "node:path";
|
|
296
352
|
|
|
353
|
+
// lib/artifacts/idea-transition-task.ts
|
|
354
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
355
|
+
"Refine Idea: ",
|
|
356
|
+
"Create Task From Idea: ",
|
|
357
|
+
"Shelve Idea: "
|
|
358
|
+
];
|
|
359
|
+
function titleToFilename(title) {
|
|
360
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
361
|
+
}
|
|
362
|
+
|
|
297
363
|
// lib/markdown/markdown-utilities.ts
|
|
298
364
|
function extractTitle(content) {
|
|
299
365
|
const match = content.match(/^#\s+(.+)$/m);
|
|
@@ -362,9 +428,6 @@ function validateFilename(filePath) {
|
|
|
362
428
|
}
|
|
363
429
|
return null;
|
|
364
430
|
}
|
|
365
|
-
function titleToFilename(title) {
|
|
366
|
-
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
367
|
-
}
|
|
368
431
|
function validateTitleFilenameMatch(filePath, content) {
|
|
369
432
|
const title = extractTitle(content);
|
|
370
433
|
if (!title) {
|
|
@@ -404,6 +467,36 @@ function validateOpeningSentenceLength(filePath, content) {
|
|
|
404
467
|
}
|
|
405
468
|
return null;
|
|
406
469
|
}
|
|
470
|
+
var NON_IMPERATIVE_STARTERS = new Set([
|
|
471
|
+
"the",
|
|
472
|
+
"a",
|
|
473
|
+
"an",
|
|
474
|
+
"this",
|
|
475
|
+
"that",
|
|
476
|
+
"these",
|
|
477
|
+
"those",
|
|
478
|
+
"we",
|
|
479
|
+
"it",
|
|
480
|
+
"they",
|
|
481
|
+
"you",
|
|
482
|
+
"i"
|
|
483
|
+
]);
|
|
484
|
+
function validateImperativeOpeningSentence(filePath, content) {
|
|
485
|
+
const openingSentence = extractOpeningSentence(content);
|
|
486
|
+
if (!openingSentence) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
const firstWord = openingSentence.split(/\s/)[0].replace(/[^a-zA-Z]/g, "");
|
|
490
|
+
const lower = firstWord.toLowerCase();
|
|
491
|
+
if (NON_IMPERATIVE_STARTERS.has(lower) || lower.endsWith("ing")) {
|
|
492
|
+
const preview = openingSentence.length > 40 ? `${openingSentence.slice(0, 40)}...` : openingSentence;
|
|
493
|
+
return {
|
|
494
|
+
file: filePath,
|
|
495
|
+
message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
407
500
|
function validateTaskHeadings(filePath, content) {
|
|
408
501
|
const violations = [];
|
|
409
502
|
for (const heading of REQUIRED_HEADINGS) {
|
|
@@ -443,6 +536,61 @@ function validateLinks(filePath, content, fileSystem) {
|
|
|
443
536
|
}
|
|
444
537
|
return violations;
|
|
445
538
|
}
|
|
539
|
+
function validateIdeaOpenQuestions(filePath, content) {
|
|
540
|
+
const violations = [];
|
|
541
|
+
const lines = content.split(`
|
|
542
|
+
`);
|
|
543
|
+
let inOpenQuestions = false;
|
|
544
|
+
let currentQuestionLine = null;
|
|
545
|
+
for (let i = 0;i < lines.length; i++) {
|
|
546
|
+
const line = lines[i];
|
|
547
|
+
if (line.startsWith("## ")) {
|
|
548
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
549
|
+
violations.push({
|
|
550
|
+
file: filePath,
|
|
551
|
+
message: "Question has no options listed beneath it",
|
|
552
|
+
line: currentQuestionLine
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
inOpenQuestions = line === "## Open Questions";
|
|
556
|
+
currentQuestionLine = null;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (!inOpenQuestions)
|
|
560
|
+
continue;
|
|
561
|
+
if (line.startsWith("### ")) {
|
|
562
|
+
if (currentQuestionLine !== null) {
|
|
563
|
+
violations.push({
|
|
564
|
+
file: filePath,
|
|
565
|
+
message: "Question has no options listed beneath it",
|
|
566
|
+
line: currentQuestionLine
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
if (!line.trimEnd().endsWith("?")) {
|
|
570
|
+
violations.push({
|
|
571
|
+
file: filePath,
|
|
572
|
+
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
573
|
+
line: i + 1
|
|
574
|
+
});
|
|
575
|
+
currentQuestionLine = null;
|
|
576
|
+
} else {
|
|
577
|
+
currentQuestionLine = i + 1;
|
|
578
|
+
}
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (line.startsWith("#### ")) {
|
|
582
|
+
currentQuestionLine = null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
586
|
+
violations.push({
|
|
587
|
+
file: filePath,
|
|
588
|
+
message: "Question has no options listed beneath it",
|
|
589
|
+
line: currentQuestionLine
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return violations;
|
|
593
|
+
}
|
|
446
594
|
var SEMANTIC_RULES = [
|
|
447
595
|
{
|
|
448
596
|
section: "## Goals",
|
|
@@ -506,6 +654,26 @@ function validateSemanticLinks(filePath, content) {
|
|
|
506
654
|
}
|
|
507
655
|
return violations;
|
|
508
656
|
}
|
|
657
|
+
function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
658
|
+
const title = extractTitle(content);
|
|
659
|
+
if (!title) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
for (const prefix of IDEA_TRANSITION_PREFIXES) {
|
|
663
|
+
if (title.startsWith(prefix)) {
|
|
664
|
+
const ideaTitle = title.slice(prefix.length);
|
|
665
|
+
const ideaFilename = titleToFilename(ideaTitle);
|
|
666
|
+
if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
|
|
667
|
+
return {
|
|
668
|
+
file: filePath,
|
|
669
|
+
message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
509
677
|
function validateGoalHierarchySections(filePath, content) {
|
|
510
678
|
const violations = [];
|
|
511
679
|
for (const heading of REQUIRED_GOAL_HEADINGS) {
|
|
@@ -735,6 +903,26 @@ async function lintMarkdown(dependencies) {
|
|
|
735
903
|
}
|
|
736
904
|
}
|
|
737
905
|
}
|
|
906
|
+
const ideasPath = `${dustPath}/ideas`;
|
|
907
|
+
const { files: ideaFiles } = await safeScanDir(glob, ideasPath);
|
|
908
|
+
if (ideaFiles.length > 0) {
|
|
909
|
+
context.stdout("Validating idea files in .dust/ideas/...");
|
|
910
|
+
for (const file of ideaFiles) {
|
|
911
|
+
if (!file.endsWith(".md"))
|
|
912
|
+
continue;
|
|
913
|
+
const filePath = `${ideasPath}/${file}`;
|
|
914
|
+
let content;
|
|
915
|
+
try {
|
|
916
|
+
content = await fileSystem.readFile(filePath);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (error.code === "ENOENT") {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
violations.push(...validateIdeaOpenQuestions(filePath, content));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
738
926
|
const tasksPath = `${dustPath}/tasks`;
|
|
739
927
|
const { files: taskFiles } = await safeScanDir(glob, tasksPath);
|
|
740
928
|
if (taskFiles.length > 0) {
|
|
@@ -758,6 +946,14 @@ async function lintMarkdown(dependencies) {
|
|
|
758
946
|
}
|
|
759
947
|
violations.push(...validateTaskHeadings(filePath, content));
|
|
760
948
|
violations.push(...validateSemanticLinks(filePath, content));
|
|
949
|
+
const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
|
|
950
|
+
if (imperativeViolation) {
|
|
951
|
+
violations.push(imperativeViolation);
|
|
952
|
+
}
|
|
953
|
+
const ideaTransitionViolation = validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem);
|
|
954
|
+
if (ideaTransitionViolation) {
|
|
955
|
+
violations.push(ideaTransitionViolation);
|
|
956
|
+
}
|
|
761
957
|
}
|
|
762
958
|
}
|
|
763
959
|
const goalsPath = `${dustPath}/goals`;
|
|
@@ -800,29 +996,6 @@ async function lintMarkdown(dependencies) {
|
|
|
800
996
|
}
|
|
801
997
|
|
|
802
998
|
// lib/cli/commands/check.ts
|
|
803
|
-
function createBufferedRunner(spawnFn) {
|
|
804
|
-
return {
|
|
805
|
-
run: (command, cwd) => {
|
|
806
|
-
return new Promise((resolve2) => {
|
|
807
|
-
const proc = spawnFn(command, [], { cwd, shell: true });
|
|
808
|
-
const chunks = [];
|
|
809
|
-
proc.stdout?.on("data", (data) => {
|
|
810
|
-
chunks.push(data.toString());
|
|
811
|
-
});
|
|
812
|
-
proc.stderr?.on("data", (data) => {
|
|
813
|
-
chunks.push(data.toString());
|
|
814
|
-
});
|
|
815
|
-
proc.on("close", (code) => {
|
|
816
|
-
resolve2({ exitCode: code ?? 1, output: chunks.join("") });
|
|
817
|
-
});
|
|
818
|
-
proc.on("error", (error) => {
|
|
819
|
-
resolve2({ exitCode: 1, output: error.message });
|
|
820
|
-
});
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
var defaultBufferedRunner = createBufferedRunner(spawn);
|
|
826
999
|
async function runConfiguredChecks(checks, cwd, runner) {
|
|
827
1000
|
const promises = checks.map(async (check) => {
|
|
828
1001
|
const startTime = Date.now();
|
|
@@ -893,7 +1066,7 @@ function displayResults(results, context) {
|
|
|
893
1066
|
context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
|
|
894
1067
|
return failed.length > 0 ? 1 : 0;
|
|
895
1068
|
}
|
|
896
|
-
async function check(dependencies,
|
|
1069
|
+
async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
897
1070
|
const { context, fileSystem, settings } = dependencies;
|
|
898
1071
|
if (!settings.checks || settings.checks.length === 0) {
|
|
899
1072
|
context.stderr("Error: No checks configured in .dust/config/settings.json");
|
|
@@ -912,7 +1085,7 @@ async function check(dependencies, bufferedRunner = defaultBufferedRunner) {
|
|
|
912
1085
|
if (fileSystem.exists(dustPath)) {
|
|
913
1086
|
checkPromises.push(runValidationCheck(dependencies));
|
|
914
1087
|
}
|
|
915
|
-
checkPromises.push(runConfiguredChecks(settings.checks, context.cwd,
|
|
1088
|
+
checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
|
|
916
1089
|
const promiseResults = await Promise.all(checkPromises);
|
|
917
1090
|
const results = [];
|
|
918
1091
|
for (const result of promiseResults) {
|
|
@@ -960,9 +1133,8 @@ var createTemplateCommand = (templateName) => async (dependencies) => {
|
|
|
960
1133
|
// lib/cli/commands/implement-task.ts
|
|
961
1134
|
var implementTask = createTemplateCommand("agent-implement-task");
|
|
962
1135
|
|
|
963
|
-
// lib/cli/
|
|
964
|
-
var
|
|
965
|
-
var colors = {
|
|
1136
|
+
// lib/cli/colors.ts
|
|
1137
|
+
var ANSI_COLORS = {
|
|
966
1138
|
reset: "\x1B[0m",
|
|
967
1139
|
bold: "\x1B[1m",
|
|
968
1140
|
dim: "\x1B[2m",
|
|
@@ -970,6 +1142,32 @@ var colors = {
|
|
|
970
1142
|
green: "\x1B[32m",
|
|
971
1143
|
yellow: "\x1B[33m"
|
|
972
1144
|
};
|
|
1145
|
+
var NO_COLORS = {
|
|
1146
|
+
reset: "",
|
|
1147
|
+
bold: "",
|
|
1148
|
+
dim: "",
|
|
1149
|
+
cyan: "",
|
|
1150
|
+
green: "",
|
|
1151
|
+
yellow: ""
|
|
1152
|
+
};
|
|
1153
|
+
function shouldDisableColors() {
|
|
1154
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
1155
|
+
return true;
|
|
1156
|
+
}
|
|
1157
|
+
if (process.env.TERM === "dumb") {
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
if (!process.stdout.isTTY) {
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
function getColors() {
|
|
1166
|
+
return shouldDisableColors() ? NO_COLORS : ANSI_COLORS;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// lib/cli/commands/init.ts
|
|
1170
|
+
var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
973
1171
|
function generateSettings(cwd, fileSystem) {
|
|
974
1172
|
const dustCommand = detectDustCommand(cwd, fileSystem);
|
|
975
1173
|
const testCommand = detectTestCommand(cwd, fileSystem);
|
|
@@ -985,6 +1183,7 @@ This project uses [dust](https://github.com/joshski/dust) for planning and docum
|
|
|
985
1183
|
`;
|
|
986
1184
|
async function init(dependencies) {
|
|
987
1185
|
const { context, fileSystem } = dependencies;
|
|
1186
|
+
const colors = getColors();
|
|
988
1187
|
const dustPath = `${context.cwd}/.dust`;
|
|
989
1188
|
const dustCommand = detectDustCommand(context.cwd, fileSystem);
|
|
990
1189
|
const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
|
|
@@ -1057,37 +1256,6 @@ async function init(dependencies) {
|
|
|
1057
1256
|
|
|
1058
1257
|
// lib/cli/commands/list.ts
|
|
1059
1258
|
import { basename } from "node:path";
|
|
1060
|
-
|
|
1061
|
-
// lib/cli/colors.ts
|
|
1062
|
-
var ANSI_COLORS = {
|
|
1063
|
-
reset: "\x1B[0m",
|
|
1064
|
-
bold: "\x1B[1m",
|
|
1065
|
-
dim: "\x1B[2m",
|
|
1066
|
-
cyan: "\x1B[36m"
|
|
1067
|
-
};
|
|
1068
|
-
var NO_COLORS = {
|
|
1069
|
-
reset: "",
|
|
1070
|
-
bold: "",
|
|
1071
|
-
dim: "",
|
|
1072
|
-
cyan: ""
|
|
1073
|
-
};
|
|
1074
|
-
function shouldDisableColors() {
|
|
1075
|
-
if (process.env.NO_COLOR !== undefined) {
|
|
1076
|
-
return true;
|
|
1077
|
-
}
|
|
1078
|
-
if (process.env.TERM === "dumb") {
|
|
1079
|
-
return true;
|
|
1080
|
-
}
|
|
1081
|
-
if (!process.stdout.isTTY) {
|
|
1082
|
-
return true;
|
|
1083
|
-
}
|
|
1084
|
-
return false;
|
|
1085
|
-
}
|
|
1086
|
-
function getColors() {
|
|
1087
|
-
return shouldDisableColors() ? NO_COLORS : ANSI_COLORS;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// lib/cli/commands/list.ts
|
|
1091
1259
|
var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
|
|
1092
1260
|
var SECTION_HEADERS = {
|
|
1093
1261
|
tasks: "\uD83D\uDCCB Tasks",
|
|
@@ -1149,7 +1317,7 @@ function renderHierarchy(nodes, output, prefix = "") {
|
|
|
1149
1317
|
async function list(dependencies) {
|
|
1150
1318
|
const { arguments: commandArguments, context, fileSystem } = dependencies;
|
|
1151
1319
|
const dustPath = `${context.cwd}/.dust`;
|
|
1152
|
-
const
|
|
1320
|
+
const colors = getColors();
|
|
1153
1321
|
if (!fileSystem.exists(dustPath)) {
|
|
1154
1322
|
context.stderr("Error: .dust directory not found");
|
|
1155
1323
|
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
@@ -1185,7 +1353,7 @@ async function list(dependencies) {
|
|
|
1185
1353
|
if (type === "goals") {
|
|
1186
1354
|
const hierarchy = await buildGoalHierarchy(dirPath, fileSystem);
|
|
1187
1355
|
if (hierarchy.length > 0) {
|
|
1188
|
-
context.stdout(`${
|
|
1356
|
+
context.stdout(`${colors.dim}Hierarchy:${colors.reset}`);
|
|
1189
1357
|
renderHierarchy(hierarchy, (line) => context.stdout(line));
|
|
1190
1358
|
context.stdout("");
|
|
1191
1359
|
}
|
|
@@ -1197,14 +1365,14 @@ async function list(dependencies) {
|
|
|
1197
1365
|
const openingSentence = extractOpeningSentence(content);
|
|
1198
1366
|
const relativePath = `.dust/${type}/${file}`;
|
|
1199
1367
|
if (title) {
|
|
1200
|
-
context.stdout(`${
|
|
1368
|
+
context.stdout(`${colors.bold}# ${title}${colors.reset}`);
|
|
1201
1369
|
} else {
|
|
1202
|
-
context.stdout(`${
|
|
1370
|
+
context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
|
|
1203
1371
|
}
|
|
1204
1372
|
if (openingSentence) {
|
|
1205
|
-
context.stdout(`${
|
|
1373
|
+
context.stdout(`${colors.dim}${openingSentence}${colors.reset}`);
|
|
1206
1374
|
}
|
|
1207
|
-
context.stdout(`${
|
|
1375
|
+
context.stdout(`${colors.cyan}→ ${relativePath}${colors.reset}`);
|
|
1208
1376
|
context.stdout("");
|
|
1209
1377
|
}
|
|
1210
1378
|
}
|
|
@@ -1629,7 +1797,7 @@ function extractBlockedBy(content) {
|
|
|
1629
1797
|
async function next(dependencies) {
|
|
1630
1798
|
const { context, fileSystem } = dependencies;
|
|
1631
1799
|
const dustPath = `${context.cwd}/.dust`;
|
|
1632
|
-
const
|
|
1800
|
+
const colors = getColors();
|
|
1633
1801
|
if (!fileSystem.exists(dustPath)) {
|
|
1634
1802
|
context.stderr("Error: .dust directory not found");
|
|
1635
1803
|
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
@@ -1666,11 +1834,11 @@ async function next(dependencies) {
|
|
|
1666
1834
|
for (const task of unblockedTasks) {
|
|
1667
1835
|
const parts = task.path.split("/");
|
|
1668
1836
|
const displayTitle = task.title || parts[parts.length - 1].replace(".md", "");
|
|
1669
|
-
context.stdout(`${
|
|
1837
|
+
context.stdout(`${colors.bold}# ${displayTitle}${colors.reset}`);
|
|
1670
1838
|
if (task.openingSentence) {
|
|
1671
|
-
context.stdout(`${
|
|
1839
|
+
context.stdout(`${colors.dim}${task.openingSentence}${colors.reset}`);
|
|
1672
1840
|
}
|
|
1673
|
-
context.stdout(`${
|
|
1841
|
+
context.stdout(`${colors.cyan}→ ${task.path}${colors.reset}`);
|
|
1674
1842
|
context.stdout("");
|
|
1675
1843
|
}
|
|
1676
1844
|
return { exitCode: 0 };
|
|
@@ -1693,9 +1861,10 @@ function formatEvent(event) {
|
|
|
1693
1861
|
return `\uD83D\uDE34 No tasks available. Sleeping...
|
|
1694
1862
|
`;
|
|
1695
1863
|
case "loop.tasks_found":
|
|
1696
|
-
return
|
|
1864
|
+
return `✨ Found a task. Going to work!
|
|
1865
|
+
`;
|
|
1697
1866
|
case "claude.started":
|
|
1698
|
-
return "\uD83E\uDD16 Claude
|
|
1867
|
+
return "\uD83E\uDD16 Starting Claude...";
|
|
1699
1868
|
case "claude.ended":
|
|
1700
1869
|
return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
|
|
1701
1870
|
case "claude.raw_event":
|
|
@@ -1879,7 +2048,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
1879
2048
|
const iterationOptions = {};
|
|
1880
2049
|
if (eventsUrl) {
|
|
1881
2050
|
iterationOptions.onRawEvent = (rawEvent) => {
|
|
1882
|
-
if (
|
|
2051
|
+
if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
|
|
1883
2052
|
agentSessionId = rawEvent.session_id;
|
|
1884
2053
|
}
|
|
1885
2054
|
emit({ type: "claude.raw_event", rawEvent });
|
|
@@ -1916,30 +2085,6 @@ var newTask = createTemplateCommand("agent-new-task");
|
|
|
1916
2085
|
var pickTask = createTemplateCommand("agent-pick-task");
|
|
1917
2086
|
|
|
1918
2087
|
// lib/cli/commands/pre-push.ts
|
|
1919
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
1920
|
-
function createGitRunner(spawnFn) {
|
|
1921
|
-
return {
|
|
1922
|
-
run: (gitArguments, cwd) => {
|
|
1923
|
-
return new Promise((resolve2) => {
|
|
1924
|
-
const proc = spawnFn("git", gitArguments, { cwd });
|
|
1925
|
-
const chunks = [];
|
|
1926
|
-
proc.stdout?.on("data", (data) => {
|
|
1927
|
-
chunks.push(data.toString());
|
|
1928
|
-
});
|
|
1929
|
-
proc.stderr?.on("data", (data) => {
|
|
1930
|
-
chunks.push(data.toString());
|
|
1931
|
-
});
|
|
1932
|
-
proc.on("close", (code) => {
|
|
1933
|
-
resolve2({ exitCode: code ?? 1, output: chunks.join("") });
|
|
1934
|
-
});
|
|
1935
|
-
proc.on("error", (error) => {
|
|
1936
|
-
resolve2({ exitCode: 1, output: error.message });
|
|
1937
|
-
});
|
|
1938
|
-
});
|
|
1939
|
-
}
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
var defaultGitRunner = createGitRunner(spawn2);
|
|
1943
2088
|
function parseGitDiffNameStatus(output) {
|
|
1944
2089
|
const changes = [];
|
|
1945
2090
|
const lines = output.trim().split(`
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joshski/dust",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Flow state for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dust": "./dist/dust.js"
|
|
8
8
|
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./artifacts": "./dist/artifacts.js"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"dist",
|
|
11
14
|
"bin",
|
|
@@ -24,7 +27,7 @@
|
|
|
24
27
|
"author": "joshski",
|
|
25
28
|
"license": "MIT",
|
|
26
29
|
"scripts": {
|
|
27
|
-
"build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && cp -r lib/templates templates",
|
|
30
|
+
"build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && bun build lib/artifacts/idea-transition-task.ts --target browser --outfile dist/artifacts.js && cp -r lib/templates templates",
|
|
28
31
|
"test": "vitest run",
|
|
29
32
|
"test:coverage": "vitest run --coverage",
|
|
30
33
|
"eval": "bun run ./evals/run.ts"
|
|
@@ -6,6 +6,41 @@ Follow these steps:
|
|
|
6
6
|
2. Create a new markdown file in `.dust/ideas/` with a descriptive kebab-case name (e.g., `improve-error-messages.md`)
|
|
7
7
|
3. Add a title as the first line using an H1 heading (e.g., `# Improve error messages`)
|
|
8
8
|
4. Write a brief description of the potential change or improvement
|
|
9
|
-
5.
|
|
10
|
-
6.
|
|
11
|
-
7.
|
|
9
|
+
5. If the idea has open questions, add an `## Open Questions` section (see below)
|
|
10
|
+
6. Run `{{bin}} lint markdown` to catch any issues with the idea file format
|
|
11
|
+
7. Create a single atomic commit with a message in the format "Add idea: <title>"
|
|
12
|
+
8. Push your commit to the remote repository
|
|
13
|
+
|
|
14
|
+
### Open Questions section
|
|
15
|
+
|
|
16
|
+
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:
|
|
17
|
+
|
|
18
|
+
```markdown
|
|
19
|
+
## Open Questions
|
|
20
|
+
|
|
21
|
+
### Should we take our own payments?
|
|
22
|
+
|
|
23
|
+
#### Yes, take our own payments
|
|
24
|
+
|
|
25
|
+
Lower costs and we become the seller of record, but requires a merchant account.
|
|
26
|
+
|
|
27
|
+
#### No, use a payment provider
|
|
28
|
+
|
|
29
|
+
Higher costs but simpler setup. No merchant account needed.
|
|
30
|
+
|
|
31
|
+
### Which storage backend should we use?
|
|
32
|
+
|
|
33
|
+
#### SQLite
|
|
34
|
+
|
|
35
|
+
Simple and embedded. Good for single-node deployments.
|
|
36
|
+
|
|
37
|
+
#### PostgreSQL
|
|
38
|
+
|
|
39
|
+
Scalable but requires a separate server.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rules:
|
|
43
|
+
- Questions are `###` headings and must end with `?`
|
|
44
|
+
- Options are `####` headings beneath a question
|
|
45
|
+
- Each question must have at least one option
|
|
46
|
+
- Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
|
|
@@ -17,7 +17,7 @@ Use a todo list to track your progress through these steps.
|
|
|
17
17
|
- The goal is a task description with minimal ambiguity at implementation time
|
|
18
18
|
4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)
|
|
19
19
|
5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)
|
|
20
|
-
6. Write a comprehensive description
|
|
20
|
+
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.
|
|
21
21
|
7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)
|
|
22
22
|
8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers
|
|
23
23
|
9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item
|
package/templates/help.txt
CHANGED
|
@@ -6,6 +6,41 @@ Follow these steps:
|
|
|
6
6
|
2. Create a new markdown file in `.dust/ideas/` with a descriptive kebab-case name (e.g., `improve-error-messages.md`)
|
|
7
7
|
3. Add a title as the first line using an H1 heading (e.g., `# Improve error messages`)
|
|
8
8
|
4. Write a brief description of the potential change or improvement
|
|
9
|
-
5.
|
|
10
|
-
6.
|
|
11
|
-
7.
|
|
9
|
+
5. If the idea has open questions, add an `## Open Questions` section (see below)
|
|
10
|
+
6. Run `{{bin}} lint markdown` to catch any issues with the idea file format
|
|
11
|
+
7. Create a single atomic commit with a message in the format "Add idea: <title>"
|
|
12
|
+
8. Push your commit to the remote repository
|
|
13
|
+
|
|
14
|
+
### Open Questions section
|
|
15
|
+
|
|
16
|
+
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:
|
|
17
|
+
|
|
18
|
+
```markdown
|
|
19
|
+
## Open Questions
|
|
20
|
+
|
|
21
|
+
### Should we take our own payments?
|
|
22
|
+
|
|
23
|
+
#### Yes, take our own payments
|
|
24
|
+
|
|
25
|
+
Lower costs and we become the seller of record, but requires a merchant account.
|
|
26
|
+
|
|
27
|
+
#### No, use a payment provider
|
|
28
|
+
|
|
29
|
+
Higher costs but simpler setup. No merchant account needed.
|
|
30
|
+
|
|
31
|
+
### Which storage backend should we use?
|
|
32
|
+
|
|
33
|
+
#### SQLite
|
|
34
|
+
|
|
35
|
+
Simple and embedded. Good for single-node deployments.
|
|
36
|
+
|
|
37
|
+
#### PostgreSQL
|
|
38
|
+
|
|
39
|
+
Scalable but requires a separate server.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Rules:
|
|
43
|
+
- Questions are `###` headings and must end with `?`
|
|
44
|
+
- Options are `####` headings beneath a question
|
|
45
|
+
- Each question must have at least one option
|
|
46
|
+
- Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
|
|
@@ -17,7 +17,7 @@ Use a todo list to track your progress through these steps.
|
|
|
17
17
|
- The goal is a task description with minimal ambiguity at implementation time
|
|
18
18
|
4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)
|
|
19
19
|
5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)
|
|
20
|
-
6. Write a comprehensive description
|
|
20
|
+
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.
|
|
21
21
|
7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)
|
|
22
22
|
8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers
|
|
23
23
|
9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item
|