@joshski/dust 0.1.59 → 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.
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from './cli/types';
1
+ import type { ReadableFileSystem } from '../cli/types';
2
2
  export interface IdeaOption {
3
3
  name: string;
4
4
  description: string;
@@ -22,4 +22,4 @@ export declare function parseOpenQuestions(content: string): IdeaOpenQuestion[];
22
22
  /**
23
23
  * Parses an idea markdown file into a structured Idea object.
24
24
  */
25
- export declare function parseIdea(fileSystem: FileSystem, dustPath: string, slug: string): Promise<Idea>;
25
+ export declare function parseIdea(fileSystem: ReadableFileSystem, dustPath: string, slug: string): Promise<Idea>;
@@ -1,4 +1,4 @@
1
- import type { FileSystem } from './cli/types';
1
+ import type { FileSystem, ReadableFileSystem } from '../cli/types';
2
2
  export declare const IDEA_TRANSITION_PREFIXES: string[];
3
3
  export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
4
4
  export declare const BUILD_IDEA_PREFIX = "Build Idea: ";
@@ -11,7 +11,7 @@ export interface ParsedCaptureIdeaTask {
11
11
  ideaDescription: string;
12
12
  buildItNow: boolean;
13
13
  }
14
- export declare function findAllCaptureIdeaTasks(fileSystem: FileSystem, dustPath: string): Promise<IdeaInProgress[]>;
14
+ export declare function findAllCaptureIdeaTasks(fileSystem: ReadableFileSystem, dustPath: string): Promise<IdeaInProgress[]>;
15
15
  /**
16
16
  * Converts a markdown title to the expected filename using deterministic rules:
17
17
  * 1. Convert to lowercase
@@ -28,7 +28,7 @@ export interface WorkflowTaskMatch {
28
28
  ideaSlug: string;
29
29
  taskSlug: string;
30
30
  }
31
- export declare function findWorkflowTaskForIdea(fileSystem: FileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
31
+ export declare function findWorkflowTaskForIdea(fileSystem: ReadableFileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
32
32
  export interface CreateIdeaTransitionTaskResult {
33
33
  filePath: string;
34
34
  }
@@ -49,4 +49,4 @@ export declare function createCaptureIdeaTask(fileSystem: FileSystem, dustPath:
49
49
  description: string;
50
50
  buildItNow?: boolean;
51
51
  }): Promise<CreateIdeaTransitionTaskResult>;
52
- export declare function parseCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
52
+ export declare function parseCaptureIdeaTask(fileSystem: ReadableFileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
@@ -0,0 +1,599 @@
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
+ var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
7
+ function extractOpeningSentence(content) {
8
+ const lines = content.split(`
9
+ `);
10
+ let h1Index = -1;
11
+ for (let i = 0;i < lines.length; i++) {
12
+ if (lines[i].match(/^#\s+.+$/)) {
13
+ h1Index = i;
14
+ break;
15
+ }
16
+ }
17
+ if (h1Index === -1) {
18
+ return null;
19
+ }
20
+ let paragraphStart = -1;
21
+ for (let i = h1Index + 1;i < lines.length; i++) {
22
+ const line = lines[i].trim();
23
+ if (line !== "") {
24
+ paragraphStart = i;
25
+ break;
26
+ }
27
+ }
28
+ if (paragraphStart === -1) {
29
+ return null;
30
+ }
31
+ const firstLine = lines[paragraphStart];
32
+ const trimmedFirstLine = firstLine.trim();
33
+ if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
34
+ return null;
35
+ }
36
+ let paragraph = "";
37
+ for (let i = paragraphStart;i < lines.length; i++) {
38
+ const line = lines[i].trim();
39
+ if (line === "")
40
+ break;
41
+ if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
42
+ break;
43
+ }
44
+ paragraph += (paragraph ? " " : "") + line;
45
+ }
46
+ const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
47
+ if (!sentenceMatch) {
48
+ return null;
49
+ }
50
+ return sentenceMatch[1];
51
+ }
52
+
53
+ // lib/artifacts/facts.ts
54
+ async function parseFact(fileSystem, dustPath, slug) {
55
+ const factPath = `${dustPath}/facts/${slug}.md`;
56
+ if (!fileSystem.exists(factPath)) {
57
+ throw new Error(`Fact not found: "${slug}" (expected file at ${factPath})`);
58
+ }
59
+ const content = await fileSystem.readFile(factPath);
60
+ const title = extractTitle(content);
61
+ if (!title) {
62
+ throw new Error(`Fact file has no title: ${factPath}`);
63
+ }
64
+ return {
65
+ slug,
66
+ title,
67
+ content
68
+ };
69
+ }
70
+
71
+ // lib/artifacts/ideas.ts
72
+ function parseOpenQuestions(content) {
73
+ const lines = content.split(`
74
+ `);
75
+ const questions = [];
76
+ let inOpenQuestions = false;
77
+ let currentQuestion = null;
78
+ let currentOption = null;
79
+ let descriptionLines = [];
80
+ function flushOption() {
81
+ if (currentOption) {
82
+ currentOption.description = descriptionLines.join(`
83
+ `).trim();
84
+ descriptionLines = [];
85
+ currentOption = null;
86
+ }
87
+ }
88
+ function flushQuestion() {
89
+ flushOption();
90
+ if (currentQuestion) {
91
+ questions.push(currentQuestion);
92
+ currentQuestion = null;
93
+ }
94
+ }
95
+ for (const line of lines) {
96
+ if (line.startsWith("## ")) {
97
+ if (inOpenQuestions) {
98
+ flushQuestion();
99
+ }
100
+ inOpenQuestions = line.trimEnd() === "## Open Questions";
101
+ continue;
102
+ }
103
+ if (!inOpenQuestions)
104
+ continue;
105
+ if (line.startsWith("### ")) {
106
+ flushQuestion();
107
+ currentQuestion = {
108
+ question: line.slice(4).trim(),
109
+ options: []
110
+ };
111
+ continue;
112
+ }
113
+ if (line.startsWith("#### ")) {
114
+ flushOption();
115
+ currentOption = {
116
+ name: line.slice(5).trim(),
117
+ description: ""
118
+ };
119
+ if (currentQuestion) {
120
+ currentQuestion.options.push(currentOption);
121
+ }
122
+ continue;
123
+ }
124
+ if (currentOption) {
125
+ descriptionLines.push(line);
126
+ }
127
+ }
128
+ flushQuestion();
129
+ return questions;
130
+ }
131
+ async function parseIdea(fileSystem, dustPath, slug) {
132
+ const ideaPath = `${dustPath}/ideas/${slug}.md`;
133
+ if (!fileSystem.exists(ideaPath)) {
134
+ throw new Error(`Idea not found: "${slug}" (expected file at ${ideaPath})`);
135
+ }
136
+ const content = await fileSystem.readFile(ideaPath);
137
+ const title = extractTitle(content);
138
+ if (!title) {
139
+ throw new Error(`Idea file has no title: ${ideaPath}`);
140
+ }
141
+ const openingSentence = extractOpeningSentence(content);
142
+ const openQuestions = parseOpenQuestions(content);
143
+ return {
144
+ slug,
145
+ title,
146
+ openingSentence,
147
+ content,
148
+ openQuestions
149
+ };
150
+ }
151
+
152
+ // lib/artifacts/principles.ts
153
+ function extractLinksFromSection(content, sectionHeading) {
154
+ const lines = content.split(`
155
+ `);
156
+ const links = [];
157
+ let inSection = false;
158
+ for (const line of lines) {
159
+ if (line.startsWith("## ")) {
160
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
161
+ continue;
162
+ }
163
+ if (!inSection)
164
+ continue;
165
+ if (line.startsWith("# "))
166
+ break;
167
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
168
+ if (linkMatch) {
169
+ const target = linkMatch[2];
170
+ const slugMatch = target.match(/([^/]+)\.md$/);
171
+ if (slugMatch) {
172
+ links.push(slugMatch[1]);
173
+ }
174
+ }
175
+ }
176
+ return links;
177
+ }
178
+ function extractSingleLinkFromSection(content, sectionHeading) {
179
+ const links = extractLinksFromSection(content, sectionHeading);
180
+ return links.length === 1 ? links[0] : null;
181
+ }
182
+ async function parsePrinciple(fileSystem, dustPath, slug) {
183
+ const principlePath = `${dustPath}/principles/${slug}.md`;
184
+ if (!fileSystem.exists(principlePath)) {
185
+ throw new Error(`Principle not found: "${slug}" (expected file at ${principlePath})`);
186
+ }
187
+ const content = await fileSystem.readFile(principlePath);
188
+ const title = extractTitle(content);
189
+ if (!title) {
190
+ throw new Error(`Principle file has no title: ${principlePath}`);
191
+ }
192
+ const parentPrinciple = extractSingleLinkFromSection(content, "Parent Principle");
193
+ const subPrinciples = extractLinksFromSection(content, "Sub-Principles");
194
+ return {
195
+ slug,
196
+ title,
197
+ content,
198
+ parentPrinciple,
199
+ subPrinciples
200
+ };
201
+ }
202
+
203
+ // lib/artifacts/tasks.ts
204
+ function extractLinksFromSection2(content, sectionHeading) {
205
+ const lines = content.split(`
206
+ `);
207
+ const links = [];
208
+ let inSection = false;
209
+ for (const line of lines) {
210
+ if (line.startsWith("## ")) {
211
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
212
+ continue;
213
+ }
214
+ if (!inSection)
215
+ continue;
216
+ if (line.startsWith("# "))
217
+ break;
218
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
219
+ if (linkMatch) {
220
+ const target = linkMatch[2];
221
+ const slugMatch = target.match(/([^/]+)\.md$/);
222
+ if (slugMatch) {
223
+ links.push(slugMatch[1]);
224
+ }
225
+ }
226
+ }
227
+ return links;
228
+ }
229
+ function extractDefinitionOfDone(content) {
230
+ const lines = content.split(`
231
+ `);
232
+ const items = [];
233
+ let inSection = false;
234
+ for (const line of lines) {
235
+ if (line.startsWith("## ")) {
236
+ inSection = line.trimEnd() === "## Definition of Done";
237
+ continue;
238
+ }
239
+ if (!inSection)
240
+ continue;
241
+ if (line.startsWith("# "))
242
+ break;
243
+ const checklistMatch = line.match(/^-\s+\[[x\s]\]\s+(.+)$/i);
244
+ if (checklistMatch) {
245
+ items.push(checklistMatch[1].trim());
246
+ }
247
+ }
248
+ return items;
249
+ }
250
+ async function parseTask(fileSystem, dustPath, slug) {
251
+ const taskPath = `${dustPath}/tasks/${slug}.md`;
252
+ if (!fileSystem.exists(taskPath)) {
253
+ throw new Error(`Task not found: "${slug}" (expected file at ${taskPath})`);
254
+ }
255
+ const content = await fileSystem.readFile(taskPath);
256
+ const title = extractTitle(content);
257
+ if (!title) {
258
+ throw new Error(`Task file has no title: ${taskPath}`);
259
+ }
260
+ const principles = extractLinksFromSection2(content, "Principles");
261
+ const blockedBy = extractLinksFromSection2(content, "Blocked By");
262
+ const definitionOfDone = extractDefinitionOfDone(content);
263
+ return {
264
+ slug,
265
+ title,
266
+ content,
267
+ principles,
268
+ blockedBy,
269
+ definitionOfDone
270
+ };
271
+ }
272
+
273
+ // lib/artifacts/workflow-tasks.ts
274
+ var CAPTURE_IDEA_PREFIX = "Add Idea: ";
275
+ var BUILD_IDEA_PREFIX = "Build Idea: ";
276
+ function titleToFilename(title) {
277
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
278
+ }
279
+ var WORKFLOW_TASK_TYPES = [
280
+ { type: "refine", prefix: "Refine Idea: " },
281
+ { type: "decompose-idea", prefix: "Decompose Idea: " },
282
+ { type: "shelve", prefix: "Shelve Idea: " }
283
+ ];
284
+ async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
285
+ const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
286
+ for (const { type, prefix } of WORKFLOW_TASK_TYPES) {
287
+ const filename = titleToFilename(`${prefix}${ideaTitle}`);
288
+ const filePath = `${dustPath}/tasks/${filename}`;
289
+ if (fileSystem.exists(filePath)) {
290
+ const taskSlug = filename.replace(/\.md$/, "");
291
+ return { type, ideaSlug, taskSlug };
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ async function readIdeaTitle(fileSystem, dustPath, ideaSlug) {
297
+ const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
298
+ if (!fileSystem.exists(ideaPath)) {
299
+ throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
300
+ }
301
+ const ideaContent = await fileSystem.readFile(ideaPath);
302
+ const ideaTitleMatch = ideaContent.match(/^#\s+(.+)$/m);
303
+ if (!ideaTitleMatch) {
304
+ throw new Error(`Idea file has no title: ${ideaPath}`);
305
+ }
306
+ return ideaTitleMatch[1].trim();
307
+ }
308
+ function renderResolvedQuestions(responses) {
309
+ const sections = responses.map((r) => `### ${r.question}
310
+
311
+ **Decision:** ${r.chosenOption}`);
312
+ return `## Resolved Questions
313
+
314
+ ${sections.join(`
315
+
316
+ `)}
317
+ `;
318
+ }
319
+ function renderTask(title, openingSentence, definitionOfDone, options) {
320
+ const descriptionParagraph = options?.description !== undefined ? `
321
+ ${options.description}
322
+ ` : "";
323
+ const resolvedSection = options?.resolvedQuestions && options.resolvedQuestions.length > 0 ? `
324
+ ${renderResolvedQuestions(options.resolvedQuestions)}
325
+ ` : "";
326
+ return `# ${title}
327
+
328
+ ${openingSentence}
329
+ ${descriptionParagraph}${resolvedSection}
330
+ ## Principles
331
+
332
+ (none)
333
+
334
+ ## Blocked By
335
+
336
+ (none)
337
+
338
+ ## Definition of Done
339
+
340
+ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
341
+ `)}
342
+ `;
343
+ }
344
+ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, taskOptions) {
345
+ const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
346
+ const taskTitle = `${prefix}${ideaTitle}`;
347
+ const filename = titleToFilename(taskTitle);
348
+ const filePath = `${dustPath}/tasks/${filename}`;
349
+ const openingSentence = openingSentenceTemplate(ideaTitle);
350
+ const content = renderTask(taskTitle, openingSentence, definitionOfDone, taskOptions);
351
+ await fileSystem.writeFile(filePath, content);
352
+ return { filePath };
353
+ }
354
+ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
355
+ 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/principles/\` 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.`, [
356
+ "Idea is thoroughly researched with relevant codebase context",
357
+ "Open questions are added for any ambiguous or underspecified aspects",
358
+ "Open questions follow the required heading format and focus on high-value decisions",
359
+ "Idea file is updated with findings"
360
+ ], { description });
361
+ }
362
+ async function decomposeIdea(fileSystem, dustPath, options) {
363
+ 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/principles/\` to link relevant principles and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
364
+ "One or more new tasks are created in .dust/tasks/",
365
+ "Task's Principles section links to relevant principles from .dust/principles/",
366
+ "The original idea is deleted or updated to reflect remaining scope"
367
+ ], {
368
+ description: options.description,
369
+ resolvedQuestions: options.openQuestionResponses
370
+ });
371
+ }
372
+ async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
373
+ 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 });
374
+ }
375
+ async function createCaptureIdeaTask(fileSystem, dustPath, options) {
376
+ const { title, description, buildItNow } = options;
377
+ if (!title || !title.trim()) {
378
+ throw new Error("title is required and must not be whitespace-only");
379
+ }
380
+ if (!description || !description.trim()) {
381
+ throw new Error("description is required and must not be whitespace-only");
382
+ }
383
+ if (buildItNow) {
384
+ const taskTitle2 = `${BUILD_IDEA_PREFIX}${title}`;
385
+ const filename2 = titleToFilename(taskTitle2);
386
+ const filePath2 = `${dustPath}/tasks/${filename2}`;
387
+ const content2 = `# ${taskTitle2}
388
+
389
+ Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
390
+
391
+ ## Idea Description
392
+
393
+ ${description}
394
+
395
+ ## Principles
396
+
397
+ (none)
398
+
399
+ ## Blocked By
400
+
401
+ (none)
402
+
403
+ ## Definition of Done
404
+
405
+ - [ ] One or more new tasks are created in \`.dust/tasks/\`
406
+ - [ ] Tasks link to relevant principles from \`.dust/principles/\`
407
+ - [ ] Tasks are narrowly scoped vertical slices
408
+ `;
409
+ await fileSystem.writeFile(filePath2, content2);
410
+ return { filePath: filePath2 };
411
+ }
412
+ const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
413
+ const filename = titleToFilename(taskTitle);
414
+ const filePath = `${dustPath}/tasks/${filename}`;
415
+ const ideaFilename = titleToFilename(title);
416
+ const ideaPath = `.dust/ideas/${ideaFilename}`;
417
+ const content = `# ${taskTitle}
418
+
419
+ 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/principles/\` and \`.dust/facts/\` for relevant context.
420
+
421
+ ## Idea Description
422
+
423
+ ${description}
424
+
425
+ ## Principles
426
+
427
+ (none)
428
+
429
+ ## Blocked By
430
+
431
+ (none)
432
+
433
+ ## Definition of Done
434
+
435
+ - [ ] Idea file exists at ${ideaPath}
436
+ - [ ] Idea file has an H1 title matching "${title}"
437
+ - [ ] Idea includes relevant context from codebase exploration
438
+ - [ ] Open questions are added for any ambiguous or underspecified aspects
439
+ - [ ] Open questions follow the required heading format and focus on high-value decisions
440
+ `;
441
+ await fileSystem.writeFile(filePath, content);
442
+ return { filePath };
443
+ }
444
+ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
445
+ const filePath = `${dustPath}/tasks/${taskSlug}.md`;
446
+ if (!fileSystem.exists(filePath)) {
447
+ return null;
448
+ }
449
+ const content = await fileSystem.readFile(filePath);
450
+ const titleMatch = content.match(/^#\s+(.+)$/m);
451
+ if (!titleMatch) {
452
+ return null;
453
+ }
454
+ const title = titleMatch[1].trim();
455
+ let ideaTitle;
456
+ let buildItNow;
457
+ if (title.startsWith(BUILD_IDEA_PREFIX)) {
458
+ ideaTitle = title.slice(BUILD_IDEA_PREFIX.length);
459
+ buildItNow = true;
460
+ } else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
461
+ ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
462
+ buildItNow = false;
463
+ } else {
464
+ return null;
465
+ }
466
+ const descriptionMatch = content.match(/^## Idea Description\n\n([\s\S]*?)\n\n## /m);
467
+ if (!descriptionMatch) {
468
+ return null;
469
+ }
470
+ const ideaDescription = descriptionMatch[1];
471
+ return { ideaTitle, ideaDescription, buildItNow };
472
+ }
473
+
474
+ // lib/artifacts/index.ts
475
+ function buildArtifactsRepository(fileSystem, dustPath) {
476
+ return {
477
+ async parseIdea(options) {
478
+ return parseIdea(fileSystem, dustPath, options.slug);
479
+ },
480
+ async listIdeas() {
481
+ const ideasPath = `${dustPath}/ideas`;
482
+ if (!fileSystem.exists(ideasPath)) {
483
+ return [];
484
+ }
485
+ const files = await fileSystem.readdir(ideasPath);
486
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
487
+ },
488
+ async parsePrinciple(options) {
489
+ return parsePrinciple(fileSystem, dustPath, options.slug);
490
+ },
491
+ async listPrinciples() {
492
+ const principlesPath = `${dustPath}/principles`;
493
+ if (!fileSystem.exists(principlesPath)) {
494
+ return [];
495
+ }
496
+ const files = await fileSystem.readdir(principlesPath);
497
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
498
+ },
499
+ async parseFact(options) {
500
+ return parseFact(fileSystem, dustPath, options.slug);
501
+ },
502
+ async listFacts() {
503
+ const factsPath = `${dustPath}/facts`;
504
+ if (!fileSystem.exists(factsPath)) {
505
+ return [];
506
+ }
507
+ const files = await fileSystem.readdir(factsPath);
508
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
509
+ },
510
+ async parseTask(options) {
511
+ return parseTask(fileSystem, dustPath, options.slug);
512
+ },
513
+ async listTasks() {
514
+ const tasksPath = `${dustPath}/tasks`;
515
+ if (!fileSystem.exists(tasksPath)) {
516
+ return [];
517
+ }
518
+ const files = await fileSystem.readdir(tasksPath);
519
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
520
+ },
521
+ async createRefineIdeaTask(options) {
522
+ return createRefineIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description);
523
+ },
524
+ async createDecomposeIdeaTask(options) {
525
+ return decomposeIdea(fileSystem, dustPath, options);
526
+ },
527
+ async createShelveIdeaTask(options) {
528
+ return createShelveIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description);
529
+ },
530
+ async createCaptureIdeaTask(options) {
531
+ return createCaptureIdeaTask(fileSystem, dustPath, options);
532
+ },
533
+ async findWorkflowTaskForIdea(options) {
534
+ return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
535
+ },
536
+ async parseCaptureIdeaTask(options) {
537
+ return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
538
+ }
539
+ };
540
+ }
541
+ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
542
+ return {
543
+ async parseIdea(options) {
544
+ return parseIdea(fileSystem, dustPath, options.slug);
545
+ },
546
+ async listIdeas() {
547
+ const ideasPath = `${dustPath}/ideas`;
548
+ if (!fileSystem.exists(ideasPath)) {
549
+ return [];
550
+ }
551
+ const files = await fileSystem.readdir(ideasPath);
552
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
553
+ },
554
+ async parsePrinciple(options) {
555
+ return parsePrinciple(fileSystem, dustPath, options.slug);
556
+ },
557
+ async listPrinciples() {
558
+ const principlesPath = `${dustPath}/principles`;
559
+ if (!fileSystem.exists(principlesPath)) {
560
+ return [];
561
+ }
562
+ const files = await fileSystem.readdir(principlesPath);
563
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
564
+ },
565
+ async parseFact(options) {
566
+ return parseFact(fileSystem, dustPath, options.slug);
567
+ },
568
+ async listFacts() {
569
+ const factsPath = `${dustPath}/facts`;
570
+ if (!fileSystem.exists(factsPath)) {
571
+ return [];
572
+ }
573
+ const files = await fileSystem.readdir(factsPath);
574
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
575
+ },
576
+ async parseTask(options) {
577
+ return parseTask(fileSystem, dustPath, options.slug);
578
+ },
579
+ async listTasks() {
580
+ const tasksPath = `${dustPath}/tasks`;
581
+ if (!fileSystem.exists(tasksPath)) {
582
+ return [];
583
+ }
584
+ const files = await fileSystem.readdir(tasksPath);
585
+ return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
586
+ },
587
+ async findWorkflowTaskForIdea(options) {
588
+ return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
589
+ },
590
+ async parseCaptureIdeaTask(options) {
591
+ return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
592
+ }
593
+ };
594
+ }
595
+ export {
596
+ parseOpenQuestions,
597
+ buildReadOnlyArtifactsRepository,
598
+ buildArtifactsRepository
599
+ };
@@ -13,16 +13,18 @@ export interface CommandResult {
13
13
  export interface WriteOptions {
14
14
  flag?: 'w' | 'wx';
15
15
  }
16
- export interface FileSystem {
16
+ export interface ReadableFileSystem {
17
17
  exists: (path: string) => boolean;
18
18
  readFile: (path: string) => Promise<string>;
19
+ readdir: (path: string) => Promise<string[]>;
20
+ isDirectory: (path: string) => boolean;
21
+ }
22
+ export interface FileSystem extends ReadableFileSystem {
19
23
  writeFile: (path: string, content: string, options?: WriteOptions) => Promise<void>;
20
24
  mkdir: (path: string, options?: {
21
25
  recursive?: boolean;
22
26
  }) => Promise<void>;
23
- readdir: (path: string) => Promise<string[]>;
24
27
  chmod: (path: string, mode: number) => Promise<void>;
25
- isDirectory: (path: string) => boolean;
26
28
  getFileCreationTime: (path: string) => number;
27
29
  rename: (oldPath: string, newPath: string) => Promise<void>;
28
30
  }
package/dist/dust.js CHANGED
@@ -1823,7 +1823,7 @@ import os from "node:os";
1823
1823
  import { dirname as dirname2, join as join7 } from "node:path";
1824
1824
  import { fileURLToPath } from "node:url";
1825
1825
 
1826
- // lib/workflow-tasks.ts
1826
+ // lib/artifacts/workflow-tasks.ts
1827
1827
  var IDEA_TRANSITION_PREFIXES = [
1828
1828
  "Refine Idea: ",
1829
1829
  "Decompose Idea: ",
@@ -4917,7 +4917,7 @@ function newIdeaInstructions(vars) {
4917
4917
  4. Write a brief description of the potential change or improvement
4918
4918
  5. If the idea has open questions, add an \`## Open Questions\` section (see below)
4919
4919
  6. Run \`${vars.bin} lint\` to catch any issues with the idea file format
4920
- 7. Create a single atomic commit with a message in the format "Add idea: <title>"
4920
+ 7. Create a single atomic commit with a message in the format "Create task: Add idea: <title>"
4921
4921
  8. Push your commit to the remote repository
4922
4922
 
4923
4923
  ### Open Questions section
@@ -4980,7 +4980,7 @@ function newPrincipleInstructions(vars) {
4980
4980
  - Why it matters for the project
4981
4981
  - How to evaluate whether work supports this principle
4982
4982
  5. Run \`${vars.bin} lint\` to catch any formatting issues
4983
- 6. Create a single atomic commit with a message in the format "Add principle: <title>"
4983
+ 6. Create a single atomic commit with a message in the format "Create task: Add principle: <title>"
4984
4984
  7. Push your commit to the remote repository
4985
4985
 
4986
4986
  Principles should be:
@@ -5027,7 +5027,7 @@ function newTaskInstructions(vars) {
5027
5027
  steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
5028
5028
  steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
5029
5029
  steps.push(`10. Run \`${vars.bin} lint\` to catch any issues with the task format`);
5030
- steps.push('11. Create a single atomic commit with a message in the format "Add task: <title>" that includes:');
5030
+ steps.push('11. Create a single atomic commit with a message in the format "Create task: Add task: <title>" that includes:');
5031
5031
  steps.push(" - The new task file");
5032
5032
  steps.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
5033
5033
  if (vars.isClaudeCodeWeb) {
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.59",
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
- ## Principles
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/principles/\` 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/principles/\` to link relevant principles 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 Principles section links to relevant principles from .dust/principles/",
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/principles/\` 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
- ## Principles
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 principles from \`.dust/principles/\`
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/principles/\` and \`.dust/facts/\` for relevant context.
179
-
180
- ## Idea Description
181
-
182
- ${description}
183
-
184
- ## Principles
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
- };