@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.
- package/dist/{ideas.d.ts → artifacts/ideas.d.ts} +2 -2
- package/dist/{workflow-tasks.d.ts → artifacts/workflow-tasks.d.ts} +4 -4
- package/dist/artifacts.js +599 -0
- package/dist/cli/types.d.ts +5 -3
- package/dist/dust.js +4 -4
- package/dist/types.d.ts +2 -2
- package/package.json +5 -9
- package/dist/ideas.js +0 -135
- package/dist/workflow-tasks.js +0 -244
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
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:
|
|
25
|
+
export declare function parseIdea(fileSystem: ReadableFileSystem, dustPath: string, slug: string): Promise<Idea>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FileSystem } from '
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
};
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -13,16 +13,18 @@ export interface CommandResult {
|
|
|
13
13
|
export interface WriteOptions {
|
|
14
14
|
flag?: 'w' | 'wx';
|
|
15
15
|
}
|
|
16
|
-
export interface
|
|
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.
|
|
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
|
-
"./
|
|
26
|
-
"import": "./dist/
|
|
27
|
-
"types": "./dist/
|
|
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/
|
|
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
|
-
};
|
package/dist/workflow-tasks.js
DELETED
|
@@ -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
|
-
};
|