@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/template-command.ts
1166
- var createTemplateCommand = (templateName) => async (dependencies) => {
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(loadTemplate(templateName, vars));
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 next(dependencies) {
1839
- const { context, fileSystem } = dependencies;
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
- context.stderr("Error: .dust directory not found");
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 { exitCode: 0 };
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 { exitCode: 0 };
1890
+ return { tasks: [] };
1855
1891
  }
1856
1892
  const existingTasks = new Set(mdFiles);
1857
- const unblockedTasks = [];
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
- unblockedTasks.push({ path: relativePath, title, openingSentence });
1903
+ tasks.push({ path: relativePath, title, openingSentence });
1868
1904
  }
1869
1905
  }
1870
- if (unblockedTasks.length === 0) {
1871
- return { exitCode: 0 };
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 unblockedTasks) {
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
- var pickTask = createTemplateCommand("agent-pick-task");
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) {
@@ -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.29",
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}} implement task`
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}} implement task`
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`