@joshski/dust 0.1.58 → 0.1.60

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/types.d.ts CHANGED
@@ -5,5 +5,5 @@
5
5
  * the event protocol, workflow tasks, and idea structures.
6
6
  */
7
7
  export type { AgentSessionEvent, EventMessage } from './agent-events';
8
- export type { Idea, IdeaOpenQuestion, IdeaOption } from './ideas';
9
- export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './workflow-tasks';
8
+ export type { Idea, IdeaOpenQuestion, IdeaOption } from './artifacts/ideas';
9
+ export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './artifacts/workflow-tasks';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,10 +10,6 @@
10
10
  "./types": {
11
11
  "types": "./dist/types.d.ts"
12
12
  },
13
- "./workflow-tasks": {
14
- "import": "./dist/workflow-tasks.js",
15
- "types": "./dist/workflow-tasks.d.ts"
16
- },
17
13
  "./logging": {
18
14
  "import": "./dist/logging.js",
19
15
  "types": "./dist/logging/index.d.ts"
@@ -22,9 +18,9 @@
22
18
  "import": "./dist/agents.js",
23
19
  "types": "./dist/agents/detection.d.ts"
24
20
  },
25
- "./ideas": {
26
- "import": "./dist/ideas.js",
27
- "types": "./dist/ideas.d.ts"
21
+ "./artifacts": {
22
+ "import": "./dist/artifacts.js",
23
+ "types": "./dist/artifacts.d.ts"
28
24
  },
29
25
  "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
30
26
  },
@@ -46,7 +42,7 @@
46
42
  "author": "joshski",
47
43
  "license": "MIT",
48
44
  "scripts": {
49
- "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/workflow-tasks.ts --target node --outfile dist/workflow-tasks.js && bun build lib/logging/index.ts --target node --outfile dist/logging.js && bun build lib/agents/detection.ts --target node --outfile dist/agents.js && bun build lib/ideas.ts --target node --outfile dist/ideas.js && bunx tsc --project tsconfig.build.json",
45
+ "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/logging/index.ts --target node --outfile dist/logging.js && bun build lib/agents/detection.ts --target node --outfile dist/agents.js && bun build lib/artifacts/index.ts --target node --outfile dist/artifacts.js && bunx tsc --project tsconfig.build.json",
50
46
  "test": "vitest run",
51
47
  "test:coverage": "vitest run --coverage",
52
48
  "eval": "bun run ./evals/run.ts"
package/dist/ideas.js DELETED
@@ -1,135 +0,0 @@
1
- // lib/markdown/markdown-utilities.ts
2
- function extractTitle(content) {
3
- const match = content.match(/^#\s+(.+)$/m);
4
- return match ? match[1].trim() : null;
5
- }
6
- function extractOpeningSentence(content) {
7
- const lines = content.split(`
8
- `);
9
- let h1Index = -1;
10
- for (let i = 0;i < lines.length; i++) {
11
- if (lines[i].match(/^#\s+.+$/)) {
12
- h1Index = i;
13
- break;
14
- }
15
- }
16
- if (h1Index === -1) {
17
- return null;
18
- }
19
- let paragraphStart = -1;
20
- for (let i = h1Index + 1;i < lines.length; i++) {
21
- const line = lines[i].trim();
22
- if (line !== "") {
23
- paragraphStart = i;
24
- break;
25
- }
26
- }
27
- if (paragraphStart === -1) {
28
- return null;
29
- }
30
- const firstLine = lines[paragraphStart];
31
- const trimmedFirstLine = firstLine.trim();
32
- if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
33
- return null;
34
- }
35
- let paragraph = "";
36
- for (let i = paragraphStart;i < lines.length; i++) {
37
- const line = lines[i].trim();
38
- if (line === "")
39
- break;
40
- if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
41
- break;
42
- }
43
- paragraph += (paragraph ? " " : "") + line;
44
- }
45
- const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
46
- if (!sentenceMatch) {
47
- return null;
48
- }
49
- return sentenceMatch[1];
50
- }
51
-
52
- // lib/ideas.ts
53
- function parseOpenQuestions(content) {
54
- const lines = content.split(`
55
- `);
56
- const questions = [];
57
- let inOpenQuestions = false;
58
- let currentQuestion = null;
59
- let currentOption = null;
60
- let descriptionLines = [];
61
- function flushOption() {
62
- if (currentOption) {
63
- currentOption.description = descriptionLines.join(`
64
- `).trim();
65
- descriptionLines = [];
66
- currentOption = null;
67
- }
68
- }
69
- function flushQuestion() {
70
- flushOption();
71
- if (currentQuestion) {
72
- questions.push(currentQuestion);
73
- currentQuestion = null;
74
- }
75
- }
76
- for (const line of lines) {
77
- if (line.startsWith("## ")) {
78
- if (inOpenQuestions) {
79
- flushQuestion();
80
- }
81
- inOpenQuestions = line.trimEnd() === "## Open Questions";
82
- continue;
83
- }
84
- if (!inOpenQuestions)
85
- continue;
86
- if (line.startsWith("### ")) {
87
- flushQuestion();
88
- currentQuestion = {
89
- question: line.slice(4).trim(),
90
- options: []
91
- };
92
- continue;
93
- }
94
- if (line.startsWith("#### ")) {
95
- flushOption();
96
- currentOption = {
97
- name: line.slice(5).trim(),
98
- description: ""
99
- };
100
- if (currentQuestion) {
101
- currentQuestion.options.push(currentOption);
102
- }
103
- continue;
104
- }
105
- if (currentOption) {
106
- descriptionLines.push(line);
107
- }
108
- }
109
- flushQuestion();
110
- return questions;
111
- }
112
- async function parseIdea(fileSystem, dustPath, slug) {
113
- const ideaPath = `${dustPath}/ideas/${slug}.md`;
114
- if (!fileSystem.exists(ideaPath)) {
115
- throw new Error(`Idea not found: "${slug}" (expected file at ${ideaPath})`);
116
- }
117
- const content = await fileSystem.readFile(ideaPath);
118
- const title = extractTitle(content);
119
- if (!title) {
120
- throw new Error(`Idea file has no title: ${ideaPath}`);
121
- }
122
- const openingSentence = extractOpeningSentence(content);
123
- const openQuestions = parseOpenQuestions(content);
124
- return {
125
- slug,
126
- title,
127
- openingSentence,
128
- content,
129
- openQuestions
130
- };
131
- }
132
- export {
133
- parseOpenQuestions,
134
- parseIdea
135
- };
@@ -1,244 +0,0 @@
1
- // lib/workflow-tasks.ts
2
- var IDEA_TRANSITION_PREFIXES = [
3
- "Refine Idea: ",
4
- "Decompose Idea: ",
5
- "Shelve Idea: "
6
- ];
7
- var CAPTURE_IDEA_PREFIX = "Add Idea: ";
8
- var BUILD_IDEA_PREFIX = "Build Idea: ";
9
- async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
10
- const tasksPath = `${dustPath}/tasks`;
11
- if (!fileSystem.exists(tasksPath))
12
- return [];
13
- const files = await fileSystem.readdir(tasksPath);
14
- const results = [];
15
- for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
16
- const content = await fileSystem.readFile(`${tasksPath}/${file}`);
17
- const titleMatch = content.match(/^#\s+(.+)$/m);
18
- if (!titleMatch)
19
- continue;
20
- const title = titleMatch[1].trim();
21
- if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
22
- results.push({
23
- taskSlug: file.replace(/\.md$/, ""),
24
- ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
25
- });
26
- } else if (title.startsWith(BUILD_IDEA_PREFIX)) {
27
- results.push({
28
- taskSlug: file.replace(/\.md$/, ""),
29
- ideaTitle: title.slice(BUILD_IDEA_PREFIX.length)
30
- });
31
- }
32
- }
33
- return results;
34
- }
35
- function titleToFilename(title) {
36
- return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
37
- }
38
- var WORKFLOW_TASK_TYPES = [
39
- { type: "refine", prefix: "Refine Idea: " },
40
- { type: "decompose-idea", prefix: "Decompose Idea: " },
41
- { type: "shelve", prefix: "Shelve Idea: " }
42
- ];
43
- async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
44
- const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
45
- for (const { type, prefix } of WORKFLOW_TASK_TYPES) {
46
- const filename = titleToFilename(`${prefix}${ideaTitle}`);
47
- const filePath = `${dustPath}/tasks/${filename}`;
48
- if (fileSystem.exists(filePath)) {
49
- const taskSlug = filename.replace(/\.md$/, "");
50
- return { type, ideaSlug, taskSlug };
51
- }
52
- }
53
- return null;
54
- }
55
- async function readIdeaTitle(fileSystem, dustPath, ideaSlug) {
56
- const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
57
- if (!fileSystem.exists(ideaPath)) {
58
- throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
59
- }
60
- const ideaContent = await fileSystem.readFile(ideaPath);
61
- const ideaTitleMatch = ideaContent.match(/^#\s+(.+)$/m);
62
- if (!ideaTitleMatch) {
63
- throw new Error(`Idea file has no title: ${ideaPath}`);
64
- }
65
- return ideaTitleMatch[1].trim();
66
- }
67
- function renderResolvedQuestions(responses) {
68
- const sections = responses.map((r) => `### ${r.question}
69
-
70
- **Decision:** ${r.chosenOption}`);
71
- return `## Resolved Questions
72
-
73
- ${sections.join(`
74
-
75
- `)}
76
- `;
77
- }
78
- function renderTask(title, openingSentence, definitionOfDone, options) {
79
- const descriptionParagraph = options?.description !== undefined ? `
80
- ${options.description}
81
- ` : "";
82
- const resolvedSection = options?.resolvedQuestions && options.resolvedQuestions.length > 0 ? `
83
- ${renderResolvedQuestions(options.resolvedQuestions)}
84
- ` : "";
85
- return `# ${title}
86
-
87
- ${openingSentence}
88
- ${descriptionParagraph}${resolvedSection}
89
- ## Goals
90
-
91
- (none)
92
-
93
- ## Blocked By
94
-
95
- (none)
96
-
97
- ## Definition of Done
98
-
99
- ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
100
- `)}
101
- `;
102
- }
103
- async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, taskOptions) {
104
- const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
105
- const taskTitle = `${prefix}${ideaTitle}`;
106
- const filename = titleToFilename(taskTitle);
107
- const filePath = `${dustPath}/tasks/${filename}`;
108
- const openingSentence = openingSentenceTemplate(ideaTitle);
109
- const content = renderTask(taskTitle, openingSentence, definitionOfDone, taskOptions);
110
- await fileSystem.writeFile(filePath, content);
111
- return { filePath };
112
- }
113
- async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
114
- 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). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
115
- "Idea is thoroughly researched with relevant codebase context",
116
- "Open questions are added for any ambiguous or underspecified aspects",
117
- "Open questions follow the required heading format and focus on high-value decisions",
118
- "Idea file is updated with findings"
119
- ], { description });
120
- }
121
- async function decomposeIdea(fileSystem, dustPath, options) {
122
- return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. 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).`, [
123
- "One or more new tasks are created in .dust/tasks/",
124
- "Task's Goals section links to relevant goals from .dust/goals/",
125
- "The original idea is deleted or updated to reflect remaining scope"
126
- ], {
127
- description: options.description,
128
- resolvedQuestions: options.openQuestionResponses
129
- });
130
- }
131
- async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
132
- return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], { description });
133
- }
134
- async function createCaptureIdeaTask(fileSystem, dustPath, options) {
135
- const { title, description, buildItNow } = options;
136
- if (!title || !title.trim()) {
137
- throw new Error("title is required and must not be whitespace-only");
138
- }
139
- if (!description || !description.trim()) {
140
- throw new Error("description is required and must not be whitespace-only");
141
- }
142
- if (buildItNow) {
143
- const taskTitle2 = `${BUILD_IDEA_PREFIX}${title}`;
144
- const filename2 = titleToFilename(taskTitle2);
145
- const filePath2 = `${dustPath}/tasks/${filename2}`;
146
- const content2 = `# ${taskTitle2}
147
-
148
- Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
149
-
150
- ## Idea Description
151
-
152
- ${description}
153
-
154
- ## Goals
155
-
156
- (none)
157
-
158
- ## Blocked By
159
-
160
- (none)
161
-
162
- ## Definition of Done
163
-
164
- - [ ] One or more new tasks are created in \`.dust/tasks/\`
165
- - [ ] Tasks link to relevant goals from \`.dust/goals/\`
166
- - [ ] Tasks are narrowly scoped vertical slices
167
- `;
168
- await fileSystem.writeFile(filePath2, content2);
169
- return { filePath: filePath2 };
170
- }
171
- const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
172
- const filename = titleToFilename(taskTitle);
173
- const filePath = `${dustPath}/tasks/${filename}`;
174
- const ideaFilename = titleToFilename(title);
175
- const ideaPath = `.dust/ideas/${ideaFilename}`;
176
- const content = `# ${taskTitle}
177
-
178
- 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. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context.
179
-
180
- ## Idea Description
181
-
182
- ${description}
183
-
184
- ## Goals
185
-
186
- (none)
187
-
188
- ## Blocked By
189
-
190
- (none)
191
-
192
- ## Definition of Done
193
-
194
- - [ ] Idea file exists at ${ideaPath}
195
- - [ ] Idea file has an H1 title matching "${title}"
196
- - [ ] Idea includes relevant context from codebase exploration
197
- - [ ] Open questions are added for any ambiguous or underspecified aspects
198
- - [ ] Open questions follow the required heading format and focus on high-value decisions
199
- `;
200
- await fileSystem.writeFile(filePath, content);
201
- return { filePath };
202
- }
203
- async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
204
- const filePath = `${dustPath}/tasks/${taskSlug}.md`;
205
- if (!fileSystem.exists(filePath)) {
206
- return null;
207
- }
208
- const content = await fileSystem.readFile(filePath);
209
- const titleMatch = content.match(/^#\s+(.+)$/m);
210
- if (!titleMatch) {
211
- return null;
212
- }
213
- const title = titleMatch[1].trim();
214
- let ideaTitle;
215
- let buildItNow;
216
- if (title.startsWith(BUILD_IDEA_PREFIX)) {
217
- ideaTitle = title.slice(BUILD_IDEA_PREFIX.length);
218
- buildItNow = true;
219
- } else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
220
- ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
221
- buildItNow = false;
222
- } else {
223
- return null;
224
- }
225
- const descriptionMatch = content.match(/^## Idea Description\n\n([\s\S]*?)\n\n## /m);
226
- if (!descriptionMatch) {
227
- return null;
228
- }
229
- const ideaDescription = descriptionMatch[1];
230
- return { ideaTitle, ideaDescription, buildItNow };
231
- }
232
- export {
233
- titleToFilename,
234
- parseCaptureIdeaTask,
235
- findWorkflowTaskForIdea,
236
- findAllCaptureIdeaTasks,
237
- decomposeIdea,
238
- createShelveIdeaTask,
239
- createRefineIdeaTask,
240
- createCaptureIdeaTask,
241
- IDEA_TRANSITION_PREFIXES,
242
- CAPTURE_IDEA_PREFIX,
243
- BUILD_IDEA_PREFIX
244
- };