@joshski/dust 0.1.57 → 0.1.59

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.js ADDED
@@ -0,0 +1,135 @@
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
+ };
@@ -28,34 +28,53 @@
28
28
  * structure under `lib/`. For example, `lib/bucket/repository-loop.ts` uses
29
29
  * the logger name `dust.bucket.repository-loop`.
30
30
  *
31
+ * ## Per-logger file routing
32
+ *
33
+ * `createLogger(name, { file })` accepts an optional `file` override:
34
+ * - `file: "path/to/file.log"` — route this logger to a dedicated file instead
35
+ * of the global sink set by `enableFileLogs`. The path is resolved relative to
36
+ * the log directory.
37
+ * - `file: false` — suppress file output for this logger entirely, even if
38
+ * `enableFileLogs` has been called. Stdout behavior (DEBUG matching) is
39
+ * unchanged.
40
+ * - When `file` is omitted, the logger uses the global file sink (if any).
41
+ *
42
+ * Sink selection precedence: per-logger sink → global sink → no file sink.
43
+ * File sinks are cached by path so multiple loggers targeting the same file
44
+ * share one sink instance.
45
+ *
31
46
  * No external dependencies.
32
47
  */
33
48
  import { type LogSink } from './sink';
34
49
  export type LogFn = (...messages: unknown[]) => void;
50
+ export interface LoggerOptions {
51
+ /**
52
+ * Per-logger file routing override.
53
+ * - `string` — path to a dedicated log file (e.g. `"./log/custom.log"`)
54
+ * - `false` — suppress file output for this logger
55
+ * - `undefined` — use the global file sink from `enableFileLogs` (default)
56
+ */
57
+ file?: string | false;
58
+ }
59
+ export interface LoggingService {
60
+ enableFileLogs(scope: string, sinkForTesting?: LogSink): void;
61
+ createLogger(name: string, options?: LoggerOptions): LogFn;
62
+ isEnabled(name: string): boolean;
63
+ }
35
64
  /**
36
- * Activate file logging for this command. Determines the log path as follows:
37
- * - If DUST_LOG_FILE is already set (inherited from a parent process such as
38
- * `dust check`), use that path — all scopes land in the same file.
39
- * - Otherwise compute the path using DUST_LOG_DIR (if set) or `<cwd>/log`, set
40
- * DUST_LOG_FILE so that any child processes inherit the same destination, then write there.
41
- *
42
- * Pass a LogSink as the second argument to override for testing.
65
+ * Create an isolated logging service instance. All mutable state is
66
+ * encapsulated inside the returned object.
43
67
  */
44
- export declare function enableFileLogs(scope: string, _sinkForTesting?: LogSink): void;
68
+ export declare function createLoggingService(): LoggingService;
45
69
  /**
46
- * Create a named logger function. The returned function writes to:
47
- * - The active file sink (if `enableFileLogs` was called), always, no filtering.
48
- * - `process.stdout` if DEBUG is set and `name` matches the pattern.
49
- *
50
- * @param name - Logger name, e.g. `dust.bucket.loop`
70
+ * Activate file logging for this command. See {@link LoggingService.enableFileLogs}.
51
71
  */
52
- export declare function createLogger(name: string): LogFn;
72
+ export declare const enableFileLogs: LoggingService['enableFileLogs'];
53
73
  /**
54
- * Check whether a logger name would produce stdout output under the current DEBUG value.
74
+ * Create a named logger function. See {@link LoggingService.createLogger}.
55
75
  */
56
- export declare function isEnabled(name: string): boolean;
76
+ export declare const createLogger: LoggingService['createLogger'];
57
77
  /**
58
- * Reset internal state (for testing only).
59
- * Clears DUST_LOG_FILE only if this module set it (not if it was inherited).
78
+ * Check whether a logger name would produce stdout output under the current DEBUG value.
60
79
  */
61
- export declare function _reset(): void;
80
+ export declare const isEnabled: LoggingService['isEnabled'];
package/dist/logging.js CHANGED
@@ -60,55 +60,71 @@ class FileSink {
60
60
 
61
61
  // lib/logging/index.ts
62
62
  var DUST_LOG_FILE = "DUST_LOG_FILE";
63
- var patterns = null;
64
- var initialized = false;
65
- var activeFileSink = null;
66
- var ownedDustLogFile = false;
67
- function init() {
68
- if (initialized)
69
- return;
70
- initialized = true;
71
- const parsed = parsePatterns(process.env.DEBUG);
72
- patterns = parsed.length > 0 ? parsed : null;
73
- }
74
- function enableFileLogs(scope, _sinkForTesting) {
75
- const existing = process.env[DUST_LOG_FILE];
76
- const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
77
- const path = existing ?? join(logDir, `${scope}.log`);
78
- if (!existing) {
79
- process.env[DUST_LOG_FILE] = path;
80
- ownedDustLogFile = true;
63
+ function createLoggingService() {
64
+ let patterns = null;
65
+ let initialized = false;
66
+ let activeFileSink = null;
67
+ const fileSinkCache = new Map;
68
+ function init() {
69
+ if (initialized)
70
+ return;
71
+ initialized = true;
72
+ const parsed = parsePatterns(process.env.DEBUG);
73
+ patterns = parsed.length > 0 ? parsed : null;
81
74
  }
82
- activeFileSink = _sinkForTesting ?? new FileSink(path);
83
- }
84
- function createLogger(name) {
85
- return (...messages) => {
86
- init();
87
- const line = formatLine(name, messages);
88
- if (activeFileSink) {
89
- activeFileSink.write(line);
75
+ function getOrCreateFileSink(path) {
76
+ let sink = fileSinkCache.get(path);
77
+ if (!sink) {
78
+ sink = new FileSink(path);
79
+ fileSinkCache.set(path, sink);
90
80
  }
91
- if (patterns && matchesAny(name, patterns)) {
92
- process.stdout.write(line);
81
+ return sink;
82
+ }
83
+ return {
84
+ enableFileLogs(scope, sinkForTesting) {
85
+ const existing = process.env[DUST_LOG_FILE];
86
+ const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
87
+ const path = existing ?? join(logDir, `${scope}.log`);
88
+ if (!existing) {
89
+ process.env[DUST_LOG_FILE] = path;
90
+ }
91
+ activeFileSink = sinkForTesting ?? new FileSink(path);
92
+ },
93
+ createLogger(name, options) {
94
+ let perLoggerSink;
95
+ if (options?.file === false) {
96
+ perLoggerSink = null;
97
+ } else if (typeof options?.file === "string") {
98
+ perLoggerSink = getOrCreateFileSink(options.file);
99
+ }
100
+ return (...messages) => {
101
+ init();
102
+ const line = formatLine(name, messages);
103
+ if (perLoggerSink !== undefined) {
104
+ if (perLoggerSink !== null) {
105
+ perLoggerSink.write(line);
106
+ }
107
+ } else if (activeFileSink) {
108
+ activeFileSink.write(line);
109
+ }
110
+ if (patterns && matchesAny(name, patterns)) {
111
+ process.stdout.write(line);
112
+ }
113
+ };
114
+ },
115
+ isEnabled(name) {
116
+ init();
117
+ return patterns !== null && matchesAny(name, patterns);
93
118
  }
94
119
  };
95
120
  }
96
- function isEnabled(name) {
97
- init();
98
- return patterns !== null && matchesAny(name, patterns);
99
- }
100
- function _reset() {
101
- initialized = false;
102
- patterns = null;
103
- activeFileSink = null;
104
- if (ownedDustLogFile) {
105
- delete process.env[DUST_LOG_FILE];
106
- ownedDustLogFile = false;
107
- }
108
- }
121
+ var defaultService = createLoggingService();
122
+ var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
123
+ var createLogger = defaultService.createLogger.bind(defaultService);
124
+ var isEnabled = defaultService.isEnabled.bind(defaultService);
109
125
  export {
110
126
  isEnabled,
111
127
  enableFileLogs,
112
- createLogger,
113
- _reset
128
+ createLoggingService,
129
+ createLogger
114
130
  };
@@ -86,7 +86,7 @@ ${renderResolvedQuestions(options.resolvedQuestions)}
86
86
 
87
87
  ${openingSentence}
88
88
  ${descriptionParagraph}${resolvedSection}
89
- ## Goals
89
+ ## Principles
90
90
 
91
91
  (none)
92
92
 
@@ -111,7 +111,7 @@ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSen
111
111
  return { filePath };
112
112
  }
113
113
  async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
114
- return createIdeaTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/goals/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
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
115
  "Idea is thoroughly researched with relevant codebase context",
116
116
  "Open questions are added for any ambiguous or underspecified aspects",
117
117
  "Open questions follow the required heading format and focus on high-value decisions",
@@ -119,9 +119,9 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
119
119
  ], { description });
120
120
  }
121
121
  async function decomposeIdea(fileSystem, dustPath, options) {
122
- return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/goals/\` to link relevant goals and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
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
123
  "One or more new tasks are created in .dust/tasks/",
124
- "Task's Goals section links to relevant goals from .dust/goals/",
124
+ "Task's Principles section links to relevant principles from .dust/principles/",
125
125
  "The original idea is deleted or updated to reflect remaining scope"
126
126
  ], {
127
127
  description: options.description,
@@ -145,13 +145,13 @@ async function createCaptureIdeaTask(fileSystem, dustPath, options) {
145
145
  const filePath2 = `${dustPath}/tasks/${filename2}`;
146
146
  const content2 = `# ${taskTitle2}
147
147
 
148
- Research this idea thoroughly, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context. Each task should deliver a thin but complete vertical slice of working software.
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
149
 
150
150
  ## Idea Description
151
151
 
152
152
  ${description}
153
153
 
154
- ## Goals
154
+ ## Principles
155
155
 
156
156
  (none)
157
157
 
@@ -162,7 +162,7 @@ ${description}
162
162
  ## Definition of Done
163
163
 
164
164
  - [ ] One or more new tasks are created in \`.dust/tasks/\`
165
- - [ ] Tasks link to relevant goals from \`.dust/goals/\`
165
+ - [ ] Tasks link to relevant principles from \`.dust/principles/\`
166
166
  - [ ] Tasks are narrowly scoped vertical slices
167
167
  `;
168
168
  await fileSystem.writeFile(filePath2, content2);
@@ -175,13 +175,13 @@ ${description}
175
175
  const ideaPath = `.dust/ideas/${ideaFilename}`;
176
176
  const content = `# ${taskTitle}
177
177
 
178
- Research this idea thoroughly, then create an idea file at \`${ideaPath}\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/goals/\` and \`.dust/facts/\` for relevant context.
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
179
 
180
180
  ## Idea Description
181
181
 
182
182
  ${description}
183
183
 
184
- ## Goals
184
+ ## Principles
185
185
 
186
186
  (none)
187
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,10 @@
22
22
  "import": "./dist/agents.js",
23
23
  "types": "./dist/agents/detection.d.ts"
24
24
  },
25
+ "./ideas": {
26
+ "import": "./dist/ideas.js",
27
+ "types": "./dist/ideas.d.ts"
28
+ },
25
29
  "./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
26
30
  },
27
31
  "files": [
@@ -42,7 +46,7 @@
42
46
  "author": "joshski",
43
47
  "license": "MIT",
44
48
  "scripts": {
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/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 && bunx tsc --project tsconfig.build.json",
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",
46
50
  "test": "vitest run",
47
51
  "test:coverage": "vitest run --coverage",
48
52
  "eval": "bun run ./evals/run.ts"