@joshski/dust 0.1.68 → 0.1.70

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.
@@ -3,9 +3,9 @@ import { type Fact } from './facts';
3
3
  import { type Idea, type IdeaOpenQuestion, type IdeaOption, parseOpenQuestions } from './ideas';
4
4
  import { type Principle } from './principles';
5
5
  import { type Task } from './tasks';
6
- import { CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllCaptureIdeaTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
7
- export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, };
8
- export { CAPTURE_IDEA_PREFIX, findAllCaptureIdeaTasks, parseOpenQuestions };
6
+ import { type AllWorkflowTasks, CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllCaptureIdeaTasks, findAllWorkflowTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
7
+ export type { AllWorkflowTasks, CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, };
8
+ export { CAPTURE_IDEA_PREFIX, findAllCaptureIdeaTasks, findAllWorkflowTasks, parseOpenQuestions, };
9
9
  export type { IdeaInProgress };
10
10
  export interface ArtifactsRepository {
11
11
  parseIdea(options: {
@@ -1,7 +1,7 @@
1
1
  import type { FileSystem, ReadableFileSystem } from '../filesystem/types';
2
2
  export declare const IDEA_TRANSITION_PREFIXES: string[];
3
3
  export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
4
- export declare const BUILD_IDEA_PREFIX = "Build Idea: ";
4
+ export declare const BUILD_IDEA_PREFIX = "Decompose Idea: ";
5
5
  export interface IdeaInProgress {
6
6
  taskSlug: string;
7
7
  ideaTitle: string;
@@ -28,6 +28,11 @@ export interface WorkflowTaskMatch {
28
28
  ideaSlug: string;
29
29
  taskSlug: string;
30
30
  }
31
+ export interface AllWorkflowTasks {
32
+ captureIdeaTasks: IdeaInProgress[];
33
+ workflowTasksByIdeaSlug: Map<string, WorkflowTaskMatch>;
34
+ }
35
+ export declare function findAllWorkflowTasks(fileSystem: ReadableFileSystem, dustPath: string): Promise<AllWorkflowTasks>;
31
36
  export declare function findWorkflowTaskForIdea(fileSystem: ReadableFileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
32
37
  export interface CreateIdeaTransitionTaskResult {
33
38
  filePath: string;
package/dist/artifacts.js CHANGED
@@ -272,7 +272,7 @@ async function parseTask(fileSystem, dustPath, slug) {
272
272
 
273
273
  // lib/artifacts/workflow-tasks.ts
274
274
  var CAPTURE_IDEA_PREFIX = "Add Idea: ";
275
- var BUILD_IDEA_PREFIX = "Build Idea: ";
275
+ var BUILD_IDEA_PREFIX = "Decompose Idea: ";
276
276
  async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
277
277
  const tasksPath = `${dustPath}/tasks`;
278
278
  if (!fileSystem.exists(tasksPath))
@@ -290,7 +290,7 @@ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
290
290
  taskSlug: file.replace(/\.md$/, ""),
291
291
  ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
292
292
  });
293
- } else if (title.startsWith(BUILD_IDEA_PREFIX)) {
293
+ } else if (title.startsWith(BUILD_IDEA_PREFIX) && !content.includes("## Decomposes Idea")) {
294
294
  results.push({
295
295
  taskSlug: file.replace(/\.md$/, ""),
296
296
  ideaTitle: title.slice(BUILD_IDEA_PREFIX.length)
@@ -331,6 +331,45 @@ function extractIdeaSlugFromSection(content, sectionHeading) {
331
331
  }
332
332
  return null;
333
333
  }
334
+ async function findAllWorkflowTasks(fileSystem, dustPath) {
335
+ const tasksPath = `${dustPath}/tasks`;
336
+ const captureIdeaTasks = [];
337
+ const workflowTasksByIdeaSlug = new Map;
338
+ if (!fileSystem.exists(tasksPath)) {
339
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
340
+ }
341
+ const files = await fileSystem.readdir(tasksPath);
342
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
343
+ const content = await fileSystem.readFile(`${tasksPath}/${file}`);
344
+ const titleMatch = content.match(/^#\s+(.+)$/m);
345
+ if (!titleMatch)
346
+ continue;
347
+ const title = titleMatch[1].trim();
348
+ const taskSlug = file.replace(/\.md$/, "");
349
+ if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
350
+ captureIdeaTasks.push({
351
+ taskSlug,
352
+ ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
353
+ });
354
+ } else if (title.startsWith(BUILD_IDEA_PREFIX) && !content.includes("## Decomposes Idea")) {
355
+ captureIdeaTasks.push({
356
+ taskSlug,
357
+ ideaTitle: title.slice(BUILD_IDEA_PREFIX.length)
358
+ });
359
+ }
360
+ for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
361
+ const linkedSlug = extractIdeaSlugFromSection(content, heading);
362
+ if (linkedSlug) {
363
+ workflowTasksByIdeaSlug.set(linkedSlug, {
364
+ type,
365
+ ideaSlug: linkedSlug,
366
+ taskSlug
367
+ });
368
+ }
369
+ }
370
+ }
371
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
372
+ }
334
373
  async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
335
374
  const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
336
375
  if (!fileSystem.exists(ideaPath)) {
@@ -652,6 +691,7 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
652
691
  }
653
692
  export {
654
693
  parseOpenQuestions,
694
+ findAllWorkflowTasks,
655
695
  findAllCaptureIdeaTasks,
656
696
  buildReadOnlyArtifactsRepository,
657
697
  buildArtifactsRepository,
package/dist/audits.js CHANGED
@@ -614,6 +614,62 @@ function errorHandling() {
614
614
  - [ ] Proposed ideas for any error handling improvements identified
615
615
  `;
616
616
  }
617
+ function globalState() {
618
+ return dedent`
619
+ # Global State
620
+
621
+ Find global state and singletons that introduce hidden coupling and hurt testability.
622
+
623
+ ${ideasHint}
624
+
625
+ ## Scope
626
+
627
+ Focus on these patterns:
628
+
629
+ 1. **Module-level mutable variables** - Variables declared outside functions that can be modified
630
+ 2. **Singleton patterns** - Classes or objects that enforce a single instance (getInstance, static instance fields)
631
+ 3. **Global registries** - Maps, arrays, or sets that accumulate state across module loads
632
+ 4. **Implicit dependencies** - Functions that read from or write to module-level state instead of using parameters
633
+ 5. **Shared configuration objects** - Mutable config objects imported and modified by multiple modules
634
+ 6. **Lazy initialization with caching** - Values computed once and stored at module level
635
+
636
+ ## Analysis Steps
637
+
638
+ 1. Search for \`let\` and \`var\` declarations at module level (outside functions/classes)
639
+ 2. Look for singleton patterns: \`getInstance\`, \`static instance\`, \`export const instance\`
640
+ 3. Find module-level \`Map\`, \`Set\`, \`Array\`, or \`Object\` that gets modified
641
+ 4. Identify functions that reference module-level variables not passed as parameters
642
+ 5. Check for \`process.env\` access scattered throughout the codebase instead of centralized
643
+
644
+ ## Output
645
+
646
+ For each global state instance identified, provide:
647
+ - **Location** - File path and line number
648
+ - **Pattern** - Which category (mutable variable, singleton, registry, etc.)
649
+ - **Impact** - How this affects testability or coupling (e.g., "Tests must reset this state", "Cannot run tests in parallel")
650
+ - **Suggestion** - How to refactor (e.g., "Pass as dependency", "Use factory function", "Move to function scope")
651
+
652
+ ## Principles
653
+
654
+ - [Dependency Injection](../principles/dependency-injection.md) - Dependencies should be passed in, not accessed globally
655
+ - [Decoupled Code](../principles/decoupled-code.md) - Code should be organized into independent units
656
+ - [Test Isolation](../principles/test-isolation.md) - Tests should not affect each other
657
+
658
+ ## Blocked By
659
+
660
+ (none)
661
+
662
+ ## Definition of Done
663
+
664
+ - [ ] Searched for module-level mutable variables (let/var outside functions)
665
+ - [ ] Identified singleton patterns and getInstance methods
666
+ - [ ] Found global registries (Maps, Sets, Arrays modified at module level)
667
+ - [ ] Located functions with implicit dependencies on module-level state
668
+ - [ ] Checked for scattered process.env access
669
+ - [ ] Documented impact of each global state instance on testing
670
+ - [ ] Proposed ideas for refactoring global state to explicit dependencies
671
+ `;
672
+ }
617
673
  function ubiquitousLanguage() {
618
674
  return dedent`
619
675
  # Ubiquitous Language
@@ -675,6 +731,7 @@ var stockAuditFunctions = {
675
731
  "dead-code": deadCode,
676
732
  "error-handling": errorHandling,
677
733
  "facts-verification": factsVerification,
734
+ "global-state": globalState,
678
735
  "ideas-from-commits": ideasFromCommits,
679
736
  "ideas-from-principles": ideasFromPrinciples,
680
737
  "performance-review": performanceReview,
@@ -14,8 +14,8 @@ export { runRepositoryLoop } from './repository-loop';
14
14
  export interface Repository {
15
15
  name: string;
16
16
  gitUrl: string;
17
- url?: string;
18
- id?: string;
17
+ url: string;
18
+ id: number;
19
19
  }
20
20
  export interface RepositoryState {
21
21
  repository: Repository;
package/dist/dust.js CHANGED
@@ -275,7 +275,7 @@ async function loadSettings(cwd, fileSystem) {
275
275
  }
276
276
 
277
277
  // lib/version.ts
278
- var DUST_VERSION = "0.1.68";
278
+ var DUST_VERSION = "0.1.70";
279
279
 
280
280
  // lib/session.ts
281
281
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -537,6 +537,8 @@ ${vars.agentInstructions}` : "";
537
537
  6. **Unclear** → \`${vars.bin} help\`
538
538
  If none of the above clearly apply, run this to see all available commands.
539
539
 
540
+ Note: "tasks" here refers to dust task files in \`.dust/tasks/\`, not internal task tracking tools.
541
+
540
542
  Do NOT proceed without running one of these commands.${instructions}
541
543
  `;
542
544
  }
@@ -1159,6 +1161,62 @@ function errorHandling() {
1159
1161
  - [ ] Proposed ideas for any error handling improvements identified
1160
1162
  `;
1161
1163
  }
1164
+ function globalState() {
1165
+ return dedent`
1166
+ # Global State
1167
+
1168
+ Find global state and singletons that introduce hidden coupling and hurt testability.
1169
+
1170
+ ${ideasHint}
1171
+
1172
+ ## Scope
1173
+
1174
+ Focus on these patterns:
1175
+
1176
+ 1. **Module-level mutable variables** - Variables declared outside functions that can be modified
1177
+ 2. **Singleton patterns** - Classes or objects that enforce a single instance (getInstance, static instance fields)
1178
+ 3. **Global registries** - Maps, arrays, or sets that accumulate state across module loads
1179
+ 4. **Implicit dependencies** - Functions that read from or write to module-level state instead of using parameters
1180
+ 5. **Shared configuration objects** - Mutable config objects imported and modified by multiple modules
1181
+ 6. **Lazy initialization with caching** - Values computed once and stored at module level
1182
+
1183
+ ## Analysis Steps
1184
+
1185
+ 1. Search for \`let\` and \`var\` declarations at module level (outside functions/classes)
1186
+ 2. Look for singleton patterns: \`getInstance\`, \`static instance\`, \`export const instance\`
1187
+ 3. Find module-level \`Map\`, \`Set\`, \`Array\`, or \`Object\` that gets modified
1188
+ 4. Identify functions that reference module-level variables not passed as parameters
1189
+ 5. Check for \`process.env\` access scattered throughout the codebase instead of centralized
1190
+
1191
+ ## Output
1192
+
1193
+ For each global state instance identified, provide:
1194
+ - **Location** - File path and line number
1195
+ - **Pattern** - Which category (mutable variable, singleton, registry, etc.)
1196
+ - **Impact** - How this affects testability or coupling (e.g., "Tests must reset this state", "Cannot run tests in parallel")
1197
+ - **Suggestion** - How to refactor (e.g., "Pass as dependency", "Use factory function", "Move to function scope")
1198
+
1199
+ ## Principles
1200
+
1201
+ - [Dependency Injection](../principles/dependency-injection.md) - Dependencies should be passed in, not accessed globally
1202
+ - [Decoupled Code](../principles/decoupled-code.md) - Code should be organized into independent units
1203
+ - [Test Isolation](../principles/test-isolation.md) - Tests should not affect each other
1204
+
1205
+ ## Blocked By
1206
+
1207
+ (none)
1208
+
1209
+ ## Definition of Done
1210
+
1211
+ - [ ] Searched for module-level mutable variables (let/var outside functions)
1212
+ - [ ] Identified singleton patterns and getInstance methods
1213
+ - [ ] Found global registries (Maps, Sets, Arrays modified at module level)
1214
+ - [ ] Located functions with implicit dependencies on module-level state
1215
+ - [ ] Checked for scattered process.env access
1216
+ - [ ] Documented impact of each global state instance on testing
1217
+ - [ ] Proposed ideas for refactoring global state to explicit dependencies
1218
+ `;
1219
+ }
1162
1220
  function ubiquitousLanguage() {
1163
1221
  return dedent`
1164
1222
  # Ubiquitous Language
@@ -1220,6 +1278,7 @@ var stockAuditFunctions = {
1220
1278
  "dead-code": deadCode,
1221
1279
  "error-handling": errorHandling,
1222
1280
  "facts-verification": factsVerification,
1281
+ "global-state": globalState,
1223
1282
  "ideas-from-commits": ideasFromCommits,
1224
1283
  "ideas-from-principles": ideasFromPrinciples,
1225
1284
  "performance-review": performanceReview,
@@ -2226,7 +2285,7 @@ var IDEA_TRANSITION_PREFIXES = [
2226
2285
  "Decompose Idea: ",
2227
2286
  "Shelve Idea: "
2228
2287
  ];
2229
- var BUILD_IDEA_PREFIX = "Build Idea: ";
2288
+ var BUILD_IDEA_PREFIX = "Decompose Idea: ";
2230
2289
  function titleToFilename(title) {
2231
2290
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
2232
2291
  }
@@ -2811,7 +2870,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2811
2870
  result = await runOneIteration(commandDeps, loopDeps, onLoopEvent, onAgentEvent, {
2812
2871
  hooksInstalled,
2813
2872
  signal: abortController.signal,
2814
- repositoryId: repoState.repository.id,
2873
+ repositoryId: repoState.repository.id.toString(),
2815
2874
  onRawEvent: (rawEvent) => {
2816
2875
  onAgentEvent(rawEventToAgentEvent(rawEvent));
2817
2876
  }
@@ -2870,23 +2929,15 @@ function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2870
2929
  });
2871
2930
  }
2872
2931
  function parseRepository(data) {
2873
- if (typeof data === "string") {
2874
- return { name: data, gitUrl: data };
2875
- }
2876
2932
  if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
2877
2933
  const repositoryData = data;
2878
- if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
2879
- const repo = {
2934
+ if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string" && typeof repositoryData.url === "string" && typeof repositoryData.id === "number") {
2935
+ return {
2880
2936
  name: repositoryData.name,
2881
- gitUrl: repositoryData.gitUrl
2937
+ gitUrl: repositoryData.gitUrl,
2938
+ url: repositoryData.url,
2939
+ id: repositoryData.id
2882
2940
  };
2883
- if (typeof repositoryData.url === "string") {
2884
- repo.url = repositoryData.url;
2885
- }
2886
- if (typeof repositoryData.id === "string") {
2887
- repo.id = repositoryData.id;
2888
- }
2889
- return repo;
2890
2941
  }
2891
2942
  }
2892
2943
  return null;
@@ -2991,20 +3042,16 @@ function parseServerMessage(data) {
2991
3042
  if (typeof repo.name !== "string" || typeof repo.gitUrl !== "string") {
2992
3043
  return null;
2993
3044
  }
2994
- const item = {
2995
- name: repo.name,
2996
- gitUrl: repo.gitUrl
2997
- };
2998
- if (typeof repo.url === "string") {
2999
- item.url = repo.url;
3000
- }
3001
- if (typeof repo.id === "string") {
3002
- item.id = repo.id;
3003
- }
3004
- if (typeof repo.hasTask === "boolean") {
3005
- item.hasTask = repo.hasTask;
3045
+ if (typeof repo.id !== "number" || typeof repo.url !== "string" || typeof repo.hasTask !== "boolean") {
3046
+ return null;
3006
3047
  }
3007
- repositories.push(item);
3048
+ repositories.push({
3049
+ id: repo.id,
3050
+ name: repo.name,
3051
+ gitUrl: repo.gitUrl,
3052
+ url: repo.url,
3053
+ hasTask: repo.hasTask
3054
+ });
3008
3055
  }
3009
3056
  return { type: "repository-list", repositories };
3010
3057
  }
@@ -3741,16 +3788,10 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
3741
3788
  for (const r of repos) {
3742
3789
  const attrs = [];
3743
3790
  attrs.push(`name=${r.name}`);
3744
- if (r.id !== undefined) {
3745
- attrs.push(`id=${r.id}`);
3746
- }
3791
+ attrs.push(`id=${r.id}`);
3747
3792
  attrs.push(`gitUrl=${r.gitUrl}`);
3748
- if (r.url !== undefined) {
3749
- attrs.push(`url=${r.url}`);
3750
- }
3751
- if (r.hasTask !== undefined) {
3752
- attrs.push(`hasTask=${r.hasTask}`);
3753
- }
3793
+ attrs.push(`url=${r.url}`);
3794
+ attrs.push(`hasTask=${r.hasTask}`);
3754
3795
  logMessage(state, context, useTUI, ` - ${attrs.join(", ")}`);
3755
3796
  }
3756
3797
  }
@@ -3997,14 +4038,15 @@ function createDefaultUploadDependencies() {
3997
4038
  const file = Bun.file(path);
3998
4039
  return file.exists();
3999
4040
  },
4000
- uploadFile: async (url, token, fileBytes, contentType) => {
4041
+ uploadFile: async (url, token, fileBytes, contentType, fileName) => {
4042
+ const formData = new FormData;
4043
+ formData.append("file", new Blob([fileBytes.buffer], { type: contentType }), fileName);
4001
4044
  const response = await fetch(url, {
4002
4045
  method: "POST",
4003
4046
  headers: {
4004
- Authorization: `Bearer ${token}`,
4005
- "Content-Type": contentType
4047
+ Authorization: `Bearer ${token}`
4006
4048
  },
4007
- body: new Blob([fileBytes])
4049
+ body: formData
4008
4050
  });
4009
4051
  if (!response.ok) {
4010
4052
  const text = await response.text();
@@ -4089,9 +4131,10 @@ async function bucketAssetUpload(dependencies, uploadDeps = createDefaultUploadD
4089
4131
  }
4090
4132
  const fileBytes = await uploadDeps.readFileBytes(filePath);
4091
4133
  const contentType = getContentType(filePath);
4134
+ const fileName = filePath.split("/").pop();
4092
4135
  const uploadUrl = `${getDustbucketHost()}/api/assets?repositoryId=${encodeURIComponent(repositoryId)}`;
4093
4136
  try {
4094
- const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType);
4137
+ const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType, fileName);
4095
4138
  context.stdout(result.url);
4096
4139
  return { exitCode: 0 };
4097
4140
  } catch (error) {
@@ -5036,7 +5079,7 @@ async function runSingleCheck(check, cwd, runner) {
5036
5079
  output: result.output,
5037
5080
  hints: check.hints,
5038
5081
  durationMs,
5039
- timedOut: result.timedOut,
5082
+ timedOut: result.timedOut ?? false,
5040
5083
  timeoutSeconds: timeoutMs / 1000
5041
5084
  };
5042
5085
  }
@@ -5075,7 +5118,8 @@ async function runValidationCheck(dependencies) {
5075
5118
  output: outputLines.join(`
5076
5119
  `),
5077
5120
  isBuiltIn: true,
5078
- durationMs
5121
+ durationMs,
5122
+ timedOut: false
5079
5123
  };
5080
5124
  }
5081
5125
  function displayResults(results, context) {
@@ -5085,7 +5129,7 @@ function displayResults(results, context) {
5085
5129
  if (result.timedOut) {
5086
5130
  context.stdout(`✗ ${result.name} [timed out after ${result.timeoutSeconds}s]`);
5087
5131
  } else {
5088
- const timing = result.durationMs !== undefined && result.durationMs >= 1000 ? ` [${(result.durationMs / 1000).toFixed(1)}s]` : "";
5132
+ const timing = result.durationMs >= 1000 ? ` [${(result.durationMs / 1000).toFixed(1)}s]` : "";
5089
5133
  if (result.exitCode === 0) {
5090
5134
  context.stdout(`✓ ${result.name}${timing}`);
5091
5135
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.68",
3
+ "version": "0.1.70",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {