@joshski/dust 0.1.30 → 0.1.32

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
@@ -435,7 +435,7 @@ function extractOpeningSentence(content) {
435
435
  // lib/workflow-tasks.ts
436
436
  var IDEA_TRANSITION_PREFIXES = [
437
437
  "Refine Idea: ",
438
- "Create Task From Idea: ",
438
+ "Decompose Idea: ",
439
439
  "Shelve Idea: "
440
440
  ];
441
441
  function titleToFilename(title) {
@@ -589,6 +589,14 @@ function validateIdeaOpenQuestions(filePath, content) {
589
589
  line: currentQuestionLine
590
590
  });
591
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
+ }
592
600
  inOpenQuestions = line === "## Open Questions";
593
601
  currentQuestionLine = null;
594
602
  continue;
@@ -1042,25 +1050,33 @@ async function lintMarkdown(dependencies) {
1042
1050
 
1043
1051
  // lib/cli/commands/check.ts
1044
1052
  var DEFAULT_CHECK_TIMEOUT_MS = 13000;
1053
+ async function runSingleCheck(check, cwd, runner) {
1054
+ const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
1055
+ const startTime = Date.now();
1056
+ const result = await runner.run(check.command, cwd, timeoutMs);
1057
+ const durationMs = Date.now() - startTime;
1058
+ return {
1059
+ name: check.name,
1060
+ command: check.command,
1061
+ exitCode: result.exitCode,
1062
+ output: result.output,
1063
+ hints: check.hints,
1064
+ durationMs,
1065
+ timedOut: result.timedOut,
1066
+ timeoutSeconds: timeoutMs / 1000
1067
+ };
1068
+ }
1045
1069
  async function runConfiguredChecks(checks, cwd, runner) {
1046
- const promises = checks.map(async (check) => {
1047
- const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
1048
- const startTime = Date.now();
1049
- const result = await runner.run(check.command, cwd, timeoutMs);
1050
- const durationMs = Date.now() - startTime;
1051
- return {
1052
- name: check.name,
1053
- command: check.command,
1054
- exitCode: result.exitCode,
1055
- output: result.output,
1056
- hints: check.hints,
1057
- durationMs,
1058
- timedOut: result.timedOut,
1059
- timeoutSeconds: timeoutMs / 1000
1060
- };
1061
- });
1070
+ const promises = checks.map((check) => runSingleCheck(check, cwd, runner));
1062
1071
  return Promise.all(promises);
1063
1072
  }
1073
+ async function runConfiguredChecksSerially(checks, cwd, runner) {
1074
+ const results = [];
1075
+ for (const check of checks) {
1076
+ results.push(await runSingleCheck(check, cwd, runner));
1077
+ }
1078
+ return results;
1079
+ }
1064
1080
  async function runValidationCheck(dependencies) {
1065
1081
  const outputLines = [];
1066
1082
  const bufferedContext = {
@@ -1123,7 +1139,13 @@ function displayResults(results, context) {
1123
1139
  return failed.length > 0 ? 1 : 0;
1124
1140
  }
1125
1141
  async function check(dependencies, shellRunner = defaultShellRunner) {
1126
- const { context, fileSystem, settings } = dependencies;
1142
+ const {
1143
+ arguments: commandArguments,
1144
+ context,
1145
+ fileSystem,
1146
+ settings
1147
+ } = dependencies;
1148
+ const serial = commandArguments.includes("--serial");
1127
1149
  if (!settings.checks || settings.checks.length === 0) {
1128
1150
  context.stderr("Error: No checks configured in .dust/config/settings.json");
1129
1151
  context.stderr("");
@@ -1136,9 +1158,20 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
1136
1158
  context.stderr(" }");
1137
1159
  return { exitCode: 1 };
1138
1160
  }
1139
- const checkPromises = [];
1140
1161
  const dustPath = `${context.cwd}/.dust`;
1141
- if (fileSystem.exists(dustPath)) {
1162
+ const hasDustDir = fileSystem.exists(dustPath);
1163
+ if (serial) {
1164
+ const results2 = [];
1165
+ if (hasDustDir) {
1166
+ results2.push(await runValidationCheck(dependencies));
1167
+ }
1168
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner);
1169
+ results2.push(...configuredResults);
1170
+ const exitCode2 = displayResults(results2, context);
1171
+ return { exitCode: exitCode2 };
1172
+ }
1173
+ const checkPromises = [];
1174
+ if (hasDustDir) {
1142
1175
  checkPromises.push(runValidationCheck(dependencies));
1143
1176
  }
1144
1177
  checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
@@ -16,12 +16,13 @@ export declare function findAllCaptureIdeaTasks(fileSystem: FileSystem, dustPath
16
16
  * 6. Add .md extension
17
17
  */
18
18
  export declare function titleToFilename(title: string): string;
19
- export type WorkflowTaskType = 'refine' | 'create-task' | 'shelve';
19
+ export type WorkflowTaskType = 'refine' | 'decompose-idea' | 'shelve';
20
20
  export interface WorkflowTaskMatch {
21
21
  type: WorkflowTaskType;
22
+ ideaSlug: string;
22
23
  taskSlug: string;
23
24
  }
24
- export declare function findWorkflowTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
25
+ export declare function findWorkflowTaskForIdea(fileSystem: FileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
25
26
  export interface CreateIdeaTransitionTaskResult {
26
27
  filePath: string;
27
28
  }
@@ -29,12 +30,12 @@ export interface OpenQuestionResponse {
29
30
  question: string;
30
31
  chosenOption: string;
31
32
  }
32
- export interface CreateTaskFromIdeaOptions {
33
+ export interface DecomposeIdeaOptions {
33
34
  ideaSlug: string;
34
35
  description?: string;
35
36
  openQuestionResponses?: OpenQuestionResponse[];
36
37
  }
37
38
  export declare function createRefineIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
38
- export declare function createTaskFromIdea(fileSystem: FileSystem, dustPath: string, options: CreateTaskFromIdeaOptions): Promise<CreateIdeaTransitionTaskResult>;
39
+ export declare function decomposeIdea(fileSystem: FileSystem, dustPath: string, options: DecomposeIdeaOptions): Promise<CreateIdeaTransitionTaskResult>;
39
40
  export declare function createShelveIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
40
41
  export declare function createCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, title: string, description: string): Promise<CreateIdeaTransitionTaskResult>;
@@ -1,7 +1,7 @@
1
1
  // lib/workflow-tasks.ts
2
2
  var IDEA_TRANSITION_PREFIXES = [
3
3
  "Refine Idea: ",
4
- "Create Task From Idea: ",
4
+ "Decompose Idea: ",
5
5
  "Shelve Idea: "
6
6
  ];
7
7
  var CAPTURE_IDEA_PREFIX = "Add Idea: ";
@@ -31,17 +31,17 @@ function titleToFilename(title) {
31
31
  }
32
32
  var WORKFLOW_TASK_TYPES = [
33
33
  { type: "refine", prefix: "Refine Idea: " },
34
- { type: "create-task", prefix: "Create Task From Idea: " },
34
+ { type: "decompose-idea", prefix: "Decompose Idea: " },
35
35
  { type: "shelve", prefix: "Shelve Idea: " }
36
36
  ];
37
- async function findWorkflowTask(fileSystem, dustPath, ideaSlug) {
37
+ async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
38
38
  const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
39
39
  for (const { type, prefix } of WORKFLOW_TASK_TYPES) {
40
40
  const filename = titleToFilename(`${prefix}${ideaTitle}`);
41
41
  const filePath = `${dustPath}/tasks/${filename}`;
42
42
  if (fileSystem.exists(filePath)) {
43
43
  const taskSlug = filename.replace(/\.md$/, "");
44
- return { type, taskSlug };
44
+ return { type, ideaSlug, taskSlug };
45
45
  }
46
46
  }
47
47
  return null;
@@ -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
- 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).`, [
116
- "A new task is created in .dust/tasks/",
114
+ async function decomposeIdea(fileSystem, dustPath, options) {
115
+ return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks -- split the idea into multiple tasks if it covers more than one logical change. 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
+ "One or more new tasks are 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",
@@ -146,9 +147,9 @@ async function createCaptureIdeaTask(fileSystem, dustPath, title, description) {
146
147
  }
147
148
  export {
148
149
  titleToFilename,
149
- findWorkflowTask,
150
+ findWorkflowTaskForIdea,
150
151
  findAllCaptureIdeaTasks,
151
- createTaskFromIdea,
152
+ decomposeIdea,
152
153
  createShelveIdeaTask,
153
154
  createRefineIdeaTask,
154
155
  createCaptureIdeaTask,
@@ -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.30",
3
+ "version": "0.1.32",
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",
@@ -1,10 +1,4 @@
1
1
  🤖 Hello {{agentName}}, welcome to dust!
2
- {{#if agentInstructions}}
3
-
4
- ## Project Instructions
5
-
6
- {{agentInstructions}}
7
- {{/if}}
8
2
 
9
3
  CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
10
4
 
@@ -29,3 +23,9 @@ Determine the user's intent and run the matching command NOW:
29
23
  If none of the above clearly apply, run this to see all available commands.
30
24
 
31
25
  Do NOT proceed without running one of these commands.
26
+ {{#if agentInstructions}}
27
+
28
+ ---
29
+
30
+ {{agentInstructions}}
31
+ {{/if}}
@@ -1,10 +1,4 @@
1
1
  🤖 Hello {{agentName}}, welcome to dust!
2
- {{#if agentInstructions}}
3
-
4
- ## Project Instructions
5
-
6
- {{agentInstructions}}
7
- {{/if}}
8
2
 
9
3
  CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
10
4
 
@@ -29,3 +23,9 @@ Determine the user's intent and run the matching command NOW:
29
23
  If none of the above clearly apply, run this to see all available commands.
30
24
 
31
25
  Do NOT proceed without running one of these commands.
26
+ {{#if agentInstructions}}
27
+
28
+ ---
29
+
30
+ {{agentInstructions}}
31
+ {{/if}}