@joshski/dust 0.1.29 → 0.1.31
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/dust.js
CHANGED
|
@@ -572,8 +572,15 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
572
572
|
`);
|
|
573
573
|
let inOpenQuestions = false;
|
|
574
574
|
let currentQuestionLine = null;
|
|
575
|
+
let inCodeBlock = false;
|
|
575
576
|
for (let i = 0;i < lines.length; i++) {
|
|
576
577
|
const line = lines[i];
|
|
578
|
+
if (line.startsWith("```")) {
|
|
579
|
+
inCodeBlock = !inCodeBlock;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (inCodeBlock)
|
|
583
|
+
continue;
|
|
577
584
|
if (line.startsWith("## ")) {
|
|
578
585
|
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
579
586
|
violations.push({
|
|
@@ -582,12 +589,28 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
582
589
|
line: currentQuestionLine
|
|
583
590
|
});
|
|
584
591
|
}
|
|
592
|
+
const headingText = line.slice(3).trimEnd();
|
|
593
|
+
if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
|
|
594
|
+
violations.push({
|
|
595
|
+
file: filePath,
|
|
596
|
+
message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
|
|
597
|
+
line: i + 1
|
|
598
|
+
});
|
|
599
|
+
}
|
|
585
600
|
inOpenQuestions = line === "## Open Questions";
|
|
586
601
|
currentQuestionLine = null;
|
|
587
602
|
continue;
|
|
588
603
|
}
|
|
589
604
|
if (!inOpenQuestions)
|
|
590
605
|
continue;
|
|
606
|
+
if (/^[-*] /.test(line.trimStart())) {
|
|
607
|
+
violations.push({
|
|
608
|
+
file: filePath,
|
|
609
|
+
message: "Open Questions must use ### headings for questions and #### headings for options, not bullet points. Run `dust new idea` to see the expected format.",
|
|
610
|
+
line: i + 1
|
|
611
|
+
});
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
591
614
|
if (line.startsWith("### ")) {
|
|
592
615
|
if (currentQuestionLine !== null) {
|
|
593
616
|
violations.push({
|
|
@@ -1142,14 +1165,34 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
1142
1165
|
|
|
1143
1166
|
// lib/cli/commands/focus.ts
|
|
1144
1167
|
async function focus(dependencies) {
|
|
1145
|
-
const { context } = dependencies;
|
|
1168
|
+
const { context, settings } = dependencies;
|
|
1146
1169
|
const objective = dependencies.arguments.join(" ").trim();
|
|
1147
1170
|
if (!objective) {
|
|
1148
1171
|
context.stderr("Error: No objective provided");
|
|
1149
1172
|
context.stderr('Usage: dust focus "your objective here"');
|
|
1150
1173
|
return { exitCode: 1 };
|
|
1151
1174
|
}
|
|
1175
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
1176
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
1152
1177
|
context.stdout(`\uD83C\uDFAF Focus: ${objective}`);
|
|
1178
|
+
context.stdout("");
|
|
1179
|
+
const steps = [];
|
|
1180
|
+
let step = 1;
|
|
1181
|
+
steps.push(`${step}. Run \`${vars.bin} check\` to verify the project is in a good state`);
|
|
1182
|
+
step++;
|
|
1183
|
+
steps.push(`${step}. Implement the task`);
|
|
1184
|
+
step++;
|
|
1185
|
+
if (!hooksInstalled) {
|
|
1186
|
+
steps.push(`${step}. Run \`${vars.bin} check\` before committing`);
|
|
1187
|
+
step++;
|
|
1188
|
+
}
|
|
1189
|
+
steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of any ideas that were fully realized", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
|
|
1190
|
+
step++;
|
|
1191
|
+
steps.push(`${step}. Push your commit to the remote repository`);
|
|
1192
|
+
steps.push("");
|
|
1193
|
+
steps.push("Keep your change small and focused. One task, one commit.");
|
|
1194
|
+
context.stdout(steps.join(`
|
|
1195
|
+
`));
|
|
1153
1196
|
return { exitCode: 0 };
|
|
1154
1197
|
}
|
|
1155
1198
|
|
|
@@ -1162,17 +1205,14 @@ async function help(dependencies) {
|
|
|
1162
1205
|
return { exitCode: 0 };
|
|
1163
1206
|
}
|
|
1164
1207
|
|
|
1165
|
-
// lib/cli/
|
|
1166
|
-
|
|
1208
|
+
// lib/cli/commands/implement-task.ts
|
|
1209
|
+
async function implementTask(dependencies) {
|
|
1167
1210
|
const { context, settings } = dependencies;
|
|
1168
1211
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
1169
1212
|
const vars = templateVariables(settings, hooksInstalled);
|
|
1170
|
-
context.stdout(
|
|
1213
|
+
context.stdout(`Run \`${vars.bin} focus "<task name>"\` to set your focus and see implementation instructions.`);
|
|
1171
1214
|
return { exitCode: 0 };
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// lib/cli/commands/implement-task.ts
|
|
1175
|
-
var implementTask = createTemplateCommand("agent-implement-task");
|
|
1215
|
+
}
|
|
1176
1216
|
|
|
1177
1217
|
// lib/cli/colors.ts
|
|
1178
1218
|
var ANSI_COLORS = {
|
|
@@ -1835,26 +1875,22 @@ function extractBlockedBy(content) {
|
|
|
1835
1875
|
}
|
|
1836
1876
|
return blockers;
|
|
1837
1877
|
}
|
|
1838
|
-
async function
|
|
1839
|
-
const
|
|
1840
|
-
const dustPath = `${context.cwd}/.dust`;
|
|
1841
|
-
const colors = getColors();
|
|
1878
|
+
async function findUnblockedTasks(cwd, fileSystem) {
|
|
1879
|
+
const dustPath = `${cwd}/.dust`;
|
|
1842
1880
|
if (!fileSystem.exists(dustPath)) {
|
|
1843
|
-
|
|
1844
|
-
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
1845
|
-
return { exitCode: 1 };
|
|
1881
|
+
return { error: ".dust directory not found", tasks: [] };
|
|
1846
1882
|
}
|
|
1847
1883
|
const tasksPath = `${dustPath}/tasks`;
|
|
1848
1884
|
if (!fileSystem.exists(tasksPath)) {
|
|
1849
|
-
return {
|
|
1885
|
+
return { tasks: [] };
|
|
1850
1886
|
}
|
|
1851
1887
|
const files = await fileSystem.readdir(tasksPath);
|
|
1852
1888
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
1853
1889
|
if (mdFiles.length === 0) {
|
|
1854
|
-
return {
|
|
1890
|
+
return { tasks: [] };
|
|
1855
1891
|
}
|
|
1856
1892
|
const existingTasks = new Set(mdFiles);
|
|
1857
|
-
const
|
|
1893
|
+
const tasks = [];
|
|
1858
1894
|
for (const file of mdFiles) {
|
|
1859
1895
|
const filePath = `${tasksPath}/${file}`;
|
|
1860
1896
|
const content = await fileSystem.readFile(filePath);
|
|
@@ -1864,15 +1900,16 @@ async function next(dependencies) {
|
|
|
1864
1900
|
const title = extractTitle(content);
|
|
1865
1901
|
const openingSentence = extractOpeningSentence(content);
|
|
1866
1902
|
const relativePath = `.dust/tasks/${file}`;
|
|
1867
|
-
|
|
1903
|
+
tasks.push({ path: relativePath, title, openingSentence });
|
|
1868
1904
|
}
|
|
1869
1905
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1906
|
+
return { tasks };
|
|
1907
|
+
}
|
|
1908
|
+
function printTaskList(context, tasks) {
|
|
1909
|
+
const colors = getColors();
|
|
1873
1910
|
context.stdout("\uD83D\uDCCB Next tasks");
|
|
1874
1911
|
context.stdout("");
|
|
1875
|
-
for (const task of
|
|
1912
|
+
for (const task of tasks) {
|
|
1876
1913
|
const parts = task.path.split("/");
|
|
1877
1914
|
const displayTitle = task.title || parts[parts.length - 1].replace(".md", "");
|
|
1878
1915
|
context.stdout(`${colors.bold}# ${displayTitle}${colors.reset}`);
|
|
@@ -1882,6 +1919,19 @@ async function next(dependencies) {
|
|
|
1882
1919
|
context.stdout(`${colors.cyan}→ ${task.path}${colors.reset}`);
|
|
1883
1920
|
context.stdout("");
|
|
1884
1921
|
}
|
|
1922
|
+
}
|
|
1923
|
+
async function next(dependencies) {
|
|
1924
|
+
const { context, fileSystem } = dependencies;
|
|
1925
|
+
const result = await findUnblockedTasks(context.cwd, fileSystem);
|
|
1926
|
+
if (result.error) {
|
|
1927
|
+
context.stderr(`Error: ${result.error}`);
|
|
1928
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
1929
|
+
return { exitCode: 1 };
|
|
1930
|
+
}
|
|
1931
|
+
if (result.tasks.length === 0) {
|
|
1932
|
+
return { exitCode: 0 };
|
|
1933
|
+
}
|
|
1934
|
+
printTaskList(context, result.tasks);
|
|
1885
1935
|
return { exitCode: 0 };
|
|
1886
1936
|
}
|
|
1887
1937
|
|
|
@@ -2113,6 +2163,15 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
2113
2163
|
return { exitCode: 0 };
|
|
2114
2164
|
}
|
|
2115
2165
|
|
|
2166
|
+
// lib/cli/template-command.ts
|
|
2167
|
+
var createTemplateCommand = (templateName) => async (dependencies) => {
|
|
2168
|
+
const { context, settings } = dependencies;
|
|
2169
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
2170
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
2171
|
+
context.stdout(loadTemplate(templateName, vars));
|
|
2172
|
+
return { exitCode: 0 };
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2116
2175
|
// lib/cli/commands/new-goal.ts
|
|
2117
2176
|
var newGoal = createTemplateCommand("agent-new-goal");
|
|
2118
2177
|
|
|
@@ -2123,7 +2182,26 @@ var newIdea = createTemplateCommand("agent-new-idea");
|
|
|
2123
2182
|
var newTask = createTemplateCommand("agent-new-task");
|
|
2124
2183
|
|
|
2125
2184
|
// lib/cli/commands/pick-task.ts
|
|
2126
|
-
|
|
2185
|
+
async function pickTask(dependencies) {
|
|
2186
|
+
const { context, fileSystem, settings } = dependencies;
|
|
2187
|
+
await manageGitHooks(dependencies);
|
|
2188
|
+
const result = await findUnblockedTasks(context.cwd, fileSystem);
|
|
2189
|
+
if (result.error) {
|
|
2190
|
+
context.stderr(`Error: ${result.error}`);
|
|
2191
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
2192
|
+
return { exitCode: 1 };
|
|
2193
|
+
}
|
|
2194
|
+
if (result.tasks.length === 0) {
|
|
2195
|
+
context.stdout("No unblocked tasks found.");
|
|
2196
|
+
return { exitCode: 0 };
|
|
2197
|
+
}
|
|
2198
|
+
context.stdout("## Pick a Task");
|
|
2199
|
+
context.stdout("");
|
|
2200
|
+
printTaskList(context, result.tasks);
|
|
2201
|
+
const vars = templateVariables(settings, false);
|
|
2202
|
+
context.stdout(`Pick ONE task, read its file to understand the requirements, then run \`${vars.bin} focus "<task name>"\`.`);
|
|
2203
|
+
return { exitCode: 0 };
|
|
2204
|
+
}
|
|
2127
2205
|
|
|
2128
2206
|
// lib/cli/commands/pre-push.ts
|
|
2129
2207
|
function parseGitDiffNameStatus(output) {
|
package/dist/workflow-tasks.js
CHANGED
|
@@ -105,15 +105,16 @@ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSen
|
|
|
105
105
|
return { filePath };
|
|
106
106
|
}
|
|
107
107
|
async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
|
|
108
|
-
return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, [
|
|
108
|
+
return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/goals/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, [
|
|
109
109
|
"Idea is thoroughly researched with relevant codebase context",
|
|
110
110
|
"Open questions are added for any ambiguous or underspecified aspects",
|
|
111
111
|
"Idea file is updated with findings"
|
|
112
112
|
], { description });
|
|
113
113
|
}
|
|
114
114
|
async function createTaskFromIdea(fileSystem, dustPath, options) {
|
|
115
|
-
return createIdeaTask(fileSystem, dustPath, "Create Task From Idea: ", options.ideaSlug, (ideaTitle) => `Create a well-defined task from this idea. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
|
|
115
|
+
return createIdeaTask(fileSystem, dustPath, "Create Task From Idea: ", options.ideaSlug, (ideaTitle) => `Create a well-defined task from this idea. Review \`.dust/goals/\` to link relevant goals and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
|
|
116
116
|
"A new task is created in .dust/tasks/",
|
|
117
|
+
"Task's Goals section links to relevant goals from .dust/goals/",
|
|
117
118
|
"The original idea is deleted or updated to reflect remaining scope"
|
|
118
119
|
], {
|
|
119
120
|
description: options.description,
|
|
@@ -135,7 +136,7 @@ async function createCaptureIdeaTask(fileSystem, dustPath, title, description) {
|
|
|
135
136
|
const filePath = `${dustPath}/tasks/${filename}`;
|
|
136
137
|
const ideaFilename = titleToFilename(title);
|
|
137
138
|
const ideaPath = `.dust/ideas/${ideaFilename}`;
|
|
138
|
-
const content = renderTask(taskTitle, `Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. The idea should have the title "${title}" and start from the following description:`, [
|
|
139
|
+
const content = renderTask(taskTitle, `Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context. The idea should have the title "${title}" and start from the following description:`, [
|
|
139
140
|
`Idea file exists at ${ideaPath}`,
|
|
140
141
|
`Idea file has an H1 title matching "${title}"`,
|
|
141
142
|
"Idea includes relevant context from codebase exploration",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const { ReportBase } = require('istanbul-lib-report')
|
|
2
|
+
|
|
3
|
+
function isFull(metrics) {
|
|
4
|
+
return (
|
|
5
|
+
metrics.statements.pct === 100 &&
|
|
6
|
+
metrics.branches.pct === 100 &&
|
|
7
|
+
metrics.functions.pct === 100 &&
|
|
8
|
+
metrics.lines.pct === 100
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatMetrics(metrics) {
|
|
13
|
+
const parts = []
|
|
14
|
+
if (metrics.lines.pct < 100) parts.push(`${metrics.lines.pct}% lines`)
|
|
15
|
+
if (metrics.statements.pct < 100)
|
|
16
|
+
parts.push(`${metrics.statements.pct}% statements`)
|
|
17
|
+
if (metrics.branches.pct < 100)
|
|
18
|
+
parts.push(`${metrics.branches.pct}% branches`)
|
|
19
|
+
if (metrics.functions.pct < 100)
|
|
20
|
+
parts.push(`${metrics.functions.pct}% functions`)
|
|
21
|
+
return parts.join(', ')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getUncoveredLines(fileCoverage) {
|
|
25
|
+
const lineCoverage = fileCoverage.getLineCoverage()
|
|
26
|
+
const ranges = []
|
|
27
|
+
let rangeStart = null
|
|
28
|
+
let rangeEnd = null
|
|
29
|
+
|
|
30
|
+
for (const [lineStr, hits] of Object.entries(lineCoverage)) {
|
|
31
|
+
const line = Number.parseInt(lineStr, 10)
|
|
32
|
+
if (hits === 0) {
|
|
33
|
+
if (rangeStart === null) {
|
|
34
|
+
rangeStart = line
|
|
35
|
+
rangeEnd = line
|
|
36
|
+
} else if (line === rangeEnd + 1) {
|
|
37
|
+
rangeEnd = line
|
|
38
|
+
} else {
|
|
39
|
+
ranges.push([rangeStart, rangeEnd])
|
|
40
|
+
rangeStart = line
|
|
41
|
+
rangeEnd = line
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (rangeStart !== null) {
|
|
47
|
+
ranges.push([rangeStart, rangeEnd])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return ranges.map(([start, end]) =>
|
|
51
|
+
start === end ? `Line ${start}` : `Lines ${start}-${end}`
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class IncompleteCoverageReporter extends ReportBase {
|
|
56
|
+
execute(context) {
|
|
57
|
+
const incompleteFiles = []
|
|
58
|
+
context.getTree().visit({
|
|
59
|
+
onDetail(node) {
|
|
60
|
+
const metrics = node.getCoverageSummary()
|
|
61
|
+
if (!metrics.isEmpty() && !isFull(metrics)) {
|
|
62
|
+
incompleteFiles.push({
|
|
63
|
+
name: node.getQualifiedName(),
|
|
64
|
+
metrics,
|
|
65
|
+
fileCoverage: node.getFileCoverage(),
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (incompleteFiles.length === 0) return
|
|
72
|
+
|
|
73
|
+
const cw = context.writer.writeFile(null)
|
|
74
|
+
const count = incompleteFiles.length
|
|
75
|
+
const label = count === 1 ? '1 file has' : `${count} files have`
|
|
76
|
+
cw.println(`${label} < 100% coverage:`)
|
|
77
|
+
|
|
78
|
+
for (const file of incompleteFiles) {
|
|
79
|
+
cw.println('')
|
|
80
|
+
cw.println(`${file.name} (${formatMetrics(file.metrics)})`)
|
|
81
|
+
for (const line of getUncoveredLines(file.fileCoverage)) {
|
|
82
|
+
cw.println(`- ${line}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cw.close()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = IncompleteCoverageReporter
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joshski/dust",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "Flow state for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
"./workflow-tasks": {
|
|
11
11
|
"import": "./dist/workflow-tasks.js",
|
|
12
12
|
"types": "./dist/workflow-tasks.d.ts"
|
|
13
|
-
}
|
|
13
|
+
},
|
|
14
|
+
"./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
|
|
14
15
|
},
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
18
|
"bin",
|
|
18
|
-
"templates"
|
|
19
|
+
"templates",
|
|
20
|
+
"lib/istanbul/minimal-reporter.cjs"
|
|
19
21
|
],
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
|
@@ -13,7 +13,7 @@ Determine the user's intent and run the matching command NOW:
|
|
|
13
13
|
1. **Pick up work from the backlog** → `{{bin}} pick task`
|
|
14
14
|
User wants to start working. Examples: "work", "go", "pick a task", "what's next?"
|
|
15
15
|
|
|
16
|
-
2. **Implement a specific task** → `{{bin}}
|
|
16
|
+
2. **Implement a specific task** → `{{bin}} focus "<task name>"`
|
|
17
17
|
User mentions a particular task by name. Examples: "implement the auth task", "work on caching"
|
|
18
18
|
|
|
19
19
|
3. **Capture a new task** → `{{bin}} new task`
|
|
@@ -13,7 +13,7 @@ Determine the user's intent and run the matching command NOW:
|
|
|
13
13
|
1. **Pick up work from the backlog** → `{{bin}} pick task`
|
|
14
14
|
User wants to start working. Examples: "work", "go", "pick a task", "what's next?"
|
|
15
15
|
|
|
16
|
-
2. **Implement a specific task** → `{{bin}}
|
|
16
|
+
2. **Implement a specific task** → `{{bin}} focus "<task name>"`
|
|
17
17
|
User mentions a particular task by name. Examples: "implement the auth task", "work on caching"
|
|
18
18
|
|
|
19
19
|
3. **Capture a new task** → `{{bin}} new task`
|