@joshski/dust 0.1.76 → 0.1.78

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.
@@ -30,6 +30,7 @@ export interface EventMessage {
30
30
  timestamp: string;
31
31
  sessionId: string;
32
32
  repository: string;
33
+ repoId?: number;
33
34
  agentSessionId?: string;
34
35
  event: AgentSessionEvent;
35
36
  }
@@ -3,8 +3,19 @@ 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 { type AllWorkflowTasks, CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, 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, };
6
+ import { type AllWorkflowTasks, CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllWorkflowTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch, type WorkflowTaskType } from './workflow-tasks';
7
+ export type { AllWorkflowTasks, CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, WorkflowTaskType, };
8
+ export interface TaskGraphNode {
9
+ task: Task;
10
+ workflowType: WorkflowTaskType | null;
11
+ }
12
+ export interface TaskGraph {
13
+ nodes: TaskGraphNode[];
14
+ edges: Array<{
15
+ from: string;
16
+ to: string;
17
+ }>;
18
+ }
8
19
  export { CAPTURE_IDEA_PREFIX, findAllWorkflowTasks, parseOpenQuestions };
9
20
  export type { IdeaInProgress };
10
21
  export interface ArtifactsRepository {
@@ -49,6 +60,7 @@ export interface ArtifactsRepository {
49
60
  parseCaptureIdeaTask(options: {
50
61
  taskSlug: string;
51
62
  }): Promise<ParsedCaptureIdeaTask | null>;
63
+ buildTaskGraph(): Promise<TaskGraph>;
52
64
  }
53
65
  export declare function buildArtifactsRepository(fileSystem: FileSystem, dustPath: string): ArtifactsRepository;
54
- export declare function buildReadOnlyArtifactsRepository(fileSystem: ReadableFileSystem, dustPath: string): Pick<ArtifactsRepository, 'parseIdea' | 'listIdeas' | 'parsePrinciple' | 'listPrinciples' | 'parseFact' | 'listFacts' | 'parseTask' | 'listTasks' | 'findWorkflowTaskForIdea' | 'parseCaptureIdeaTask'>;
66
+ export declare function buildReadOnlyArtifactsRepository(fileSystem: ReadableFileSystem, dustPath: string): Pick<ArtifactsRepository, 'parseIdea' | 'listIdeas' | 'parsePrinciple' | 'listPrinciples' | 'parseFact' | 'listFacts' | 'parseTask' | 'listTasks' | 'findWorkflowTaskForIdea' | 'parseCaptureIdeaTask' | 'buildTaskGraph'>;
package/dist/artifacts.js CHANGED
@@ -609,6 +609,27 @@ function buildArtifactsRepository(fileSystem, dustPath) {
609
609
  },
610
610
  async parseCaptureIdeaTask(options) {
611
611
  return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
612
+ },
613
+ async buildTaskGraph() {
614
+ const taskSlugs = await this.listTasks();
615
+ const allWorkflowTasks = await findAllWorkflowTasks(fileSystem, dustPath);
616
+ const workflowTypeByTaskSlug = new Map;
617
+ for (const match of allWorkflowTasks.workflowTasksByIdeaSlug.values()) {
618
+ workflowTypeByTaskSlug.set(match.taskSlug, match.type);
619
+ }
620
+ const nodes = [];
621
+ const edges = [];
622
+ for (const slug of taskSlugs) {
623
+ const task = await this.parseTask({ slug });
624
+ nodes.push({
625
+ task,
626
+ workflowType: workflowTypeByTaskSlug.get(slug) ?? null
627
+ });
628
+ for (const blockerSlug of task.blockedBy) {
629
+ edges.push({ from: blockerSlug, to: slug });
630
+ }
631
+ }
632
+ return { nodes, edges };
612
633
  }
613
634
  };
614
635
  }
@@ -663,6 +684,27 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
663
684
  },
664
685
  async parseCaptureIdeaTask(options) {
665
686
  return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
687
+ },
688
+ async buildTaskGraph() {
689
+ const taskSlugs = await this.listTasks();
690
+ const allWorkflowTasks = await findAllWorkflowTasks(fileSystem, dustPath);
691
+ const workflowTypeByTaskSlug = new Map;
692
+ for (const match of allWorkflowTasks.workflowTasksByIdeaSlug.values()) {
693
+ workflowTypeByTaskSlug.set(match.taskSlug, match.type);
694
+ }
695
+ const nodes = [];
696
+ const edges = [];
697
+ for (const slug of taskSlugs) {
698
+ const task = await this.parseTask({ slug });
699
+ nodes.push({
700
+ task,
701
+ workflowType: workflowTypeByTaskSlug.get(slug) ?? null
702
+ });
703
+ for (const blockerSlug of task.blockedBy) {
704
+ edges.push({ from: blockerSlug, to: slug });
705
+ }
706
+ }
707
+ return { nodes, edges };
666
708
  }
667
709
  };
668
710
  }
package/dist/audits.js CHANGED
@@ -831,6 +831,75 @@ function ubiquitousLanguage() {
831
831
  - [ ] Proposed ideas for standardizing inconsistent terminology
832
832
  `;
833
833
  }
834
+ function uxAudit() {
835
+ return dedent`
836
+ # UX Audit
837
+
838
+ Review the end user experience by capturing visual or interactive evidence at key scenarios.
839
+
840
+ ${ideasHint}
841
+
842
+ ## Scope
843
+
844
+ 1. **Identify key scenarios** - What are the main user journeys? (e.g., signup, login, checkout, onboarding, core workflows)
845
+ 2. **Capture evidence** - For each scenario:
846
+ - Web apps: Take screenshots at each step using browser automation (Playwright, Puppeteer, Cypress, or similar)
847
+ - Terminal apps: Capture command output and interactive sessions
848
+ 3. **Review captured evidence** for UX issues:
849
+ - Confusing or unclear states
850
+ - Missing feedback or loading indicators
851
+ - Error messages that don't guide recovery
852
+ - Inconsistent styling or layout
853
+ 4. **Document findings** with screenshots/output and specific recommendations
854
+
855
+ ## Applicability
856
+
857
+ Determine the application type and available tooling:
858
+ - If browser tests exist (Playwright, Puppeteer, Cypress), extend them to capture screenshots
859
+ - If no browser tests exist, write a standalone script for key scenarios
860
+ - For terminal apps, capture representative sessions using command output or terminal recording
861
+
862
+ If the project has no user-facing interface, document that finding and skip the detailed analysis.
863
+
864
+ ## Analysis Steps
865
+
866
+ 1. Identify the application type (web, terminal, hybrid, no UI)
867
+ 2. List the key user scenarios from documentation, tests, or code analysis
868
+ 3. Capture screenshots or output at each stage of each scenario
869
+ 4. Store artifacts in a temporary directory for review during this audit
870
+ 5. Review each artifact for UX issues
871
+ 6. Document findings with evidence and specific recommendations
872
+
873
+ ## Output
874
+
875
+ For each UX issue identified, provide:
876
+ - **Location** - Which scenario and step
877
+ - **Evidence** - Screenshot filename or captured output
878
+ - **Problem** - What's wrong from the user's perspective
879
+ - **Impact** - How it affects the user's ability to complete their goal
880
+ - **Recommendation** - Specific fix
881
+ - **Verification** - How to verify the fix (e.g., "Screenshot at step 3 should show success message instead of spinner")
882
+
883
+ ## Principles
884
+
885
+ - [Actionable Errors](../principles/actionable-errors.md) - Error messages should tell users what to do next
886
+ - [Unsurprising UX](../principles/unsurprising-ux.md) - The interface should be as guessable as possible
887
+
888
+ ## Blocked By
889
+
890
+ (none)
891
+
892
+ ## Definition of Done
893
+
894
+ - [ ] Identified the application type (web, terminal, hybrid, or no UI)
895
+ - [ ] Listed key user scenarios
896
+ - [ ] Captured screenshots or output at each stage of key scenarios
897
+ - [ ] Reviewed evidence for UX issues
898
+ - [ ] Documented findings with evidence and recommendations
899
+ - [ ] Included verification criteria for each issue
900
+ - [ ] Created ideas for any UX improvements needed
901
+ `;
902
+ }
834
903
  var stockAuditFunctions = {
835
904
  "agent-developer-experience": agentDeveloperExperience,
836
905
  "component-reuse": componentReuse,
@@ -849,7 +918,8 @@ var stockAuditFunctions = {
849
918
  "slow-tests": slowTests,
850
919
  "stale-ideas": staleIdeas,
851
920
  "test-coverage": testCoverage,
852
- "ubiquitous-language": ubiquitousLanguage
921
+ "ubiquitous-language": ubiquitousLanguage,
922
+ "ux-audit": uxAudit
853
923
  };
854
924
  function loadStockAudits() {
855
925
  return Object.entries(stockAuditFunctions).sort(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
@@ -30,6 +30,7 @@ export declare function buildEventMessage(parameters: {
30
30
  sequence: number;
31
31
  sessionId: string;
32
32
  repository: string;
33
+ repoId?: number;
33
34
  event: AgentSessionEvent;
34
35
  agentSessionId?: string;
35
36
  }): EventMessage;
@@ -17,7 +17,7 @@ interface Colors {
17
17
  /**
18
18
  * Determines whether colors should be disabled based on environment.
19
19
  */
20
- export declare function shouldDisableColors(): boolean;
20
+ export declare function shouldDisableColors(env?: NodeJS.ProcessEnv): boolean;
21
21
  /**
22
22
  * Returns the appropriate colors object based on environment detection.
23
23
  */
@@ -7,5 +7,5 @@
7
7
  * Usage: dust focus "add login box"
8
8
  */
9
9
  import type { CommandDependencies, CommandResult } from '../types';
10
- export declare function buildImplementationInstructions(bin: string, hooksInstalled: boolean, taskTitle?: string): string;
10
+ export declare function buildImplementationInstructions(bin: string, hooksInstalled: boolean, taskTitle?: string, taskPath?: string, installCommand?: string): string;
11
11
  export declare function focus(dependencies: CommandDependencies): Promise<CommandResult>;
@@ -20,15 +20,14 @@ export declare function validateSettingsJson(content: string): SettingsViolation
20
20
  */
21
21
  export declare function detectDustCommand(cwd: string, fileSystem: ReadableFileSystem): string;
22
22
  /**
23
- * Detects the appropriate install command based on lockfiles and environment.
24
- * Priority:
25
- * 1. bun.lockb exists bun install
26
- * 2. pnpm-lock.yaml exists pnpm install
27
- * 3. package-lock.json exists → npm install
28
- * 4. No lockfile + BUN_INSTALL env var set → bun install
29
- * 5. Default → npm install
23
+ * Detects the appropriate install command based on lockfiles.
24
+ * Returns null when:
25
+ * - No recognized lockfile is found
26
+ * - Multiple ecosystems are detected (requires explicit configuration)
27
+ *
28
+ * Priority within each ecosystem follows the order in LOCKFILE_COMMANDS.
30
29
  */
31
- export declare function detectInstallCommand(cwd: string, fileSystem: ReadableFileSystem): string;
30
+ export declare function detectInstallCommand(cwd: string, fileSystem: ReadableFileSystem): string | null;
32
31
  /**
33
32
  * Detects the appropriate test command based on lockfiles and environment.
34
33
  * Priority:
package/dist/dust.js CHANGED
@@ -170,8 +170,7 @@ function validateSettingsJson(content) {
170
170
  return violations;
171
171
  }
172
172
  var DEFAULT_SETTINGS = {
173
- dustCommand: "npx dust",
174
- installCommand: "npm install"
173
+ dustCommand: "npx dust"
175
174
  };
176
175
  function detectDustCommand(cwd, fileSystem) {
177
176
  if (fileSystem.exists(join(cwd, "bun.lockb"))) {
@@ -188,20 +187,38 @@ function detectDustCommand(cwd, fileSystem) {
188
187
  }
189
188
  return "npx dust";
190
189
  }
190
+ var LOCKFILE_COMMANDS = [
191
+ { file: "bun.lockb", command: "bun install", ecosystem: "js" },
192
+ { file: "pnpm-lock.yaml", command: "pnpm install", ecosystem: "js" },
193
+ { file: "package-lock.json", command: "npm install", ecosystem: "js" },
194
+ { file: "Gemfile.lock", command: "bundle install", ecosystem: "ruby" },
195
+ { file: "poetry.lock", command: "poetry install", ecosystem: "python" },
196
+ { file: "Pipfile.lock", command: "pipenv install", ecosystem: "python" },
197
+ {
198
+ file: "requirements.txt",
199
+ command: "pip install -r requirements.txt",
200
+ ecosystem: "python"
201
+ },
202
+ { file: "go.sum", command: "go mod download", ecosystem: "go" },
203
+ { file: "Cargo.lock", command: "cargo build", ecosystem: "rust" },
204
+ { file: "composer.lock", command: "composer install", ecosystem: "php" },
205
+ { file: "mix.lock", command: "mix deps.get", ecosystem: "elixir" }
206
+ ];
191
207
  function detectInstallCommand(cwd, fileSystem) {
192
- if (fileSystem.exists(join(cwd, "bun.lockb"))) {
193
- return "bun install";
194
- }
195
- if (fileSystem.exists(join(cwd, "pnpm-lock.yaml"))) {
196
- return "pnpm install";
197
- }
198
- if (fileSystem.exists(join(cwd, "package-lock.json"))) {
199
- return "npm install";
208
+ const foundEcosystems = new Set;
209
+ let firstCommand = null;
210
+ for (const { file, command, ecosystem } of LOCKFILE_COMMANDS) {
211
+ if (fileSystem.exists(join(cwd, file))) {
212
+ if (firstCommand === null) {
213
+ firstCommand = command;
214
+ }
215
+ foundEcosystems.add(ecosystem);
216
+ }
200
217
  }
201
- if (process.env.BUN_INSTALL) {
202
- return "bun install";
218
+ if (foundEcosystems.size > 1) {
219
+ return null;
203
220
  }
204
- return "npm install";
221
+ return firstCommand;
205
222
  }
206
223
  function detectTestCommand(cwd, fileSystem) {
207
224
  if (fileSystem.exists(join(cwd, "bun.lockb")) || fileSystem.exists(join(cwd, "bun.lock"))) {
@@ -234,9 +251,12 @@ async function loadSettings(cwd, fileSystem) {
234
251
  const settingsPath = join(cwd, ".dust", "config", "settings.json");
235
252
  if (!fileSystem.exists(settingsPath)) {
236
253
  const result = {
237
- dustCommand: detectDustCommand(cwd, fileSystem),
238
- installCommand: detectInstallCommand(cwd, fileSystem)
254
+ dustCommand: detectDustCommand(cwd, fileSystem)
239
255
  };
256
+ const installCommand = detectInstallCommand(cwd, fileSystem);
257
+ if (installCommand !== null) {
258
+ result.installCommand = installCommand;
259
+ }
240
260
  if (process.env.DUST_EVENTS_URL) {
241
261
  result.eventsUrl = process.env.DUST_EVENTS_URL;
242
262
  }
@@ -256,26 +276,37 @@ async function loadSettings(cwd, fileSystem) {
256
276
  result.dustCommand = detectDustCommand(cwd, fileSystem);
257
277
  }
258
278
  if (!parsed.installCommand) {
259
- result.installCommand = detectInstallCommand(cwd, fileSystem);
279
+ const installCommand = detectInstallCommand(cwd, fileSystem);
280
+ if (installCommand !== null) {
281
+ result.installCommand = installCommand;
282
+ } else {
283
+ delete result.installCommand;
284
+ }
260
285
  }
261
286
  if (process.env.DUST_EVENTS_URL) {
262
287
  result.eventsUrl = process.env.DUST_EVENTS_URL;
263
288
  }
264
289
  return result;
265
- } catch {
266
- const result = {
267
- dustCommand: detectDustCommand(cwd, fileSystem),
268
- installCommand: detectInstallCommand(cwd, fileSystem)
269
- };
270
- if (process.env.DUST_EVENTS_URL) {
271
- result.eventsUrl = process.env.DUST_EVENTS_URL;
290
+ } catch (error) {
291
+ if (error.code === "ENOENT") {
292
+ const result = {
293
+ dustCommand: detectDustCommand(cwd, fileSystem)
294
+ };
295
+ const installCommand = detectInstallCommand(cwd, fileSystem);
296
+ if (installCommand !== null) {
297
+ result.installCommand = installCommand;
298
+ }
299
+ if (process.env.DUST_EVENTS_URL) {
300
+ result.eventsUrl = process.env.DUST_EVENTS_URL;
301
+ }
302
+ return result;
272
303
  }
273
- return result;
304
+ throw error;
274
305
  }
275
306
  }
276
307
 
277
308
  // lib/version.ts
278
- var DUST_VERSION = "0.1.76";
309
+ var DUST_VERSION = "0.1.78";
279
310
 
280
311
  // lib/session.ts
281
312
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -460,8 +491,11 @@ async function loadAgentInstructions(cwd, fileSystem, agentType) {
460
491
  try {
461
492
  const content = await fileSystem.readFile(instructionsPath);
462
493
  return content.trim();
463
- } catch {
464
- return "";
494
+ } catch (error) {
495
+ if (error.code === "ENOENT") {
496
+ return "";
497
+ }
498
+ throw error;
465
499
  }
466
500
  }
467
501
  function templateVariables(settings, hooksInstalled, env = process.env, options) {
@@ -1378,6 +1412,75 @@ function ubiquitousLanguage() {
1378
1412
  - [ ] Proposed ideas for standardizing inconsistent terminology
1379
1413
  `;
1380
1414
  }
1415
+ function uxAudit() {
1416
+ return dedent`
1417
+ # UX Audit
1418
+
1419
+ Review the end user experience by capturing visual or interactive evidence at key scenarios.
1420
+
1421
+ ${ideasHint}
1422
+
1423
+ ## Scope
1424
+
1425
+ 1. **Identify key scenarios** - What are the main user journeys? (e.g., signup, login, checkout, onboarding, core workflows)
1426
+ 2. **Capture evidence** - For each scenario:
1427
+ - Web apps: Take screenshots at each step using browser automation (Playwright, Puppeteer, Cypress, or similar)
1428
+ - Terminal apps: Capture command output and interactive sessions
1429
+ 3. **Review captured evidence** for UX issues:
1430
+ - Confusing or unclear states
1431
+ - Missing feedback or loading indicators
1432
+ - Error messages that don't guide recovery
1433
+ - Inconsistent styling or layout
1434
+ 4. **Document findings** with screenshots/output and specific recommendations
1435
+
1436
+ ## Applicability
1437
+
1438
+ Determine the application type and available tooling:
1439
+ - If browser tests exist (Playwright, Puppeteer, Cypress), extend them to capture screenshots
1440
+ - If no browser tests exist, write a standalone script for key scenarios
1441
+ - For terminal apps, capture representative sessions using command output or terminal recording
1442
+
1443
+ If the project has no user-facing interface, document that finding and skip the detailed analysis.
1444
+
1445
+ ## Analysis Steps
1446
+
1447
+ 1. Identify the application type (web, terminal, hybrid, no UI)
1448
+ 2. List the key user scenarios from documentation, tests, or code analysis
1449
+ 3. Capture screenshots or output at each stage of each scenario
1450
+ 4. Store artifacts in a temporary directory for review during this audit
1451
+ 5. Review each artifact for UX issues
1452
+ 6. Document findings with evidence and specific recommendations
1453
+
1454
+ ## Output
1455
+
1456
+ For each UX issue identified, provide:
1457
+ - **Location** - Which scenario and step
1458
+ - **Evidence** - Screenshot filename or captured output
1459
+ - **Problem** - What's wrong from the user's perspective
1460
+ - **Impact** - How it affects the user's ability to complete their goal
1461
+ - **Recommendation** - Specific fix
1462
+ - **Verification** - How to verify the fix (e.g., "Screenshot at step 3 should show success message instead of spinner")
1463
+
1464
+ ## Principles
1465
+
1466
+ - [Actionable Errors](../principles/actionable-errors.md) - Error messages should tell users what to do next
1467
+ - [Unsurprising UX](../principles/unsurprising-ux.md) - The interface should be as guessable as possible
1468
+
1469
+ ## Blocked By
1470
+
1471
+ (none)
1472
+
1473
+ ## Definition of Done
1474
+
1475
+ - [ ] Identified the application type (web, terminal, hybrid, or no UI)
1476
+ - [ ] Listed key user scenarios
1477
+ - [ ] Captured screenshots or output at each stage of key scenarios
1478
+ - [ ] Reviewed evidence for UX issues
1479
+ - [ ] Documented findings with evidence and recommendations
1480
+ - [ ] Included verification criteria for each issue
1481
+ - [ ] Created ideas for any UX improvements needed
1482
+ `;
1483
+ }
1381
1484
  var stockAuditFunctions = {
1382
1485
  "agent-developer-experience": agentDeveloperExperience,
1383
1486
  "component-reuse": componentReuse,
@@ -1396,7 +1499,8 @@ var stockAuditFunctions = {
1396
1499
  "slow-tests": slowTests,
1397
1500
  "stale-ideas": staleIdeas,
1398
1501
  "test-coverage": testCoverage,
1399
- "ubiquitous-language": ubiquitousLanguage
1502
+ "ubiquitous-language": ubiquitousLanguage,
1503
+ "ux-audit": uxAudit
1400
1504
  };
1401
1505
  function loadStockAudits() {
1402
1506
  return Object.entries(stockAuditFunctions).sort(([a], [b]) => a.localeCompare(b)).map(([name, render]) => {
@@ -1451,11 +1555,11 @@ var NO_COLORS = {
1451
1555
  green: "",
1452
1556
  yellow: ""
1453
1557
  };
1454
- function shouldDisableColors() {
1455
- if (process.env.NO_COLOR !== undefined) {
1558
+ function shouldDisableColors(env = process.env) {
1559
+ if (env.NO_COLOR !== undefined) {
1456
1560
  return true;
1457
1561
  }
1458
- if (process.env.TERM === "dumb") {
1562
+ if (env.TERM === "dumb") {
1459
1563
  return true;
1460
1564
  }
1461
1565
  if (!process.stdout.isTTY) {
@@ -1571,8 +1675,8 @@ var CREDENTIALS_DIR = ".dust";
1571
1675
  var CREDENTIALS_FILE = "credentials.json";
1572
1676
  var AUTH_TIMEOUT_MS = 120000;
1573
1677
  var DEFAULT_DUSTBUCKET_HOST = "https://dustbucket.com";
1574
- function getDustbucketHost() {
1575
- return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
1678
+ function getDustbucketHost(env = process.env) {
1679
+ return env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
1576
1680
  }
1577
1681
  function credentialsPath(homeDir) {
1578
1682
  return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
@@ -1583,8 +1687,11 @@ async function loadStoredToken(fileSystem, homeDir) {
1583
1687
  const content = await fileSystem.readFile(path);
1584
1688
  const data = JSON.parse(content);
1585
1689
  return typeof data.token === "string" ? data.token : null;
1586
- } catch {
1587
- return null;
1690
+ } catch (error) {
1691
+ if (error.code === "ENOENT") {
1692
+ return null;
1693
+ }
1694
+ throw error;
1588
1695
  }
1589
1696
  }
1590
1697
  async function storeToken(fileSystem, homeDir, token) {
@@ -1596,7 +1703,12 @@ async function clearToken(fileSystem, homeDir) {
1596
1703
  const path = credentialsPath(homeDir);
1597
1704
  try {
1598
1705
  await fileSystem.writeFile(path, "{}");
1599
- } catch {}
1706
+ } catch (error) {
1707
+ if (error.code === "ENOENT") {
1708
+ return;
1709
+ }
1710
+ throw error;
1711
+ }
1600
1712
  }
1601
1713
  async function defaultExchangeCode(code) {
1602
1714
  const host = getDustbucketHost();
@@ -1777,6 +1889,134 @@ import { dirname as dirname2 } from "node:path";
1777
1889
  // lib/claude/spawn-claude-code.ts
1778
1890
  import { spawn as nodeSpawn2 } from "node:child_process";
1779
1891
  import { createInterface as nodeCreateInterface } from "node:readline";
1892
+
1893
+ // lib/logging/index.ts
1894
+ import { join as join6 } from "node:path";
1895
+
1896
+ // lib/logging/match.ts
1897
+ function parsePatterns(debug) {
1898
+ if (!debug)
1899
+ return [];
1900
+ const expressions = debug.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
1901
+ return expressions.map((expr) => {
1902
+ const escaped = expr.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
1903
+ const pattern = escaped.replace(/\*/g, ".*");
1904
+ return new RegExp(`^${pattern}$`);
1905
+ });
1906
+ }
1907
+ function matchesAny(name, patterns) {
1908
+ return patterns.some((re) => re.test(name));
1909
+ }
1910
+ function formatLine(name, messages) {
1911
+ const text = messages.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
1912
+ return `${new Date().toISOString()} [${name}] ${text}
1913
+ `;
1914
+ }
1915
+
1916
+ // lib/logging/sink.ts
1917
+ import { appendFileSync, mkdirSync } from "node:fs";
1918
+ import { dirname } from "node:path";
1919
+
1920
+ class FileSink {
1921
+ logPath;
1922
+ _appendFileSync;
1923
+ _mkdirSync;
1924
+ resolvedPath;
1925
+ ready = false;
1926
+ constructor(logPath, _appendFileSync = appendFileSync, _mkdirSync = mkdirSync) {
1927
+ this.logPath = logPath;
1928
+ this._appendFileSync = _appendFileSync;
1929
+ this._mkdirSync = _mkdirSync;
1930
+ }
1931
+ ensureLogFile() {
1932
+ if (this.ready)
1933
+ return this.resolvedPath;
1934
+ this.ready = true;
1935
+ this.resolvedPath = this.logPath;
1936
+ try {
1937
+ this._mkdirSync(dirname(this.logPath), { recursive: true });
1938
+ } catch {
1939
+ this.resolvedPath = undefined;
1940
+ }
1941
+ return this.resolvedPath;
1942
+ }
1943
+ write(line) {
1944
+ const path = this.ensureLogFile();
1945
+ if (!path)
1946
+ return;
1947
+ try {
1948
+ this._appendFileSync(path, line);
1949
+ } catch {}
1950
+ }
1951
+ }
1952
+
1953
+ // lib/logging/index.ts
1954
+ var DUST_LOG_FILE = "DUST_LOG_FILE";
1955
+ function createLoggingService() {
1956
+ let patterns = null;
1957
+ let initialized = false;
1958
+ let activeFileSink = null;
1959
+ const fileSinkCache = new Map;
1960
+ function init() {
1961
+ if (initialized)
1962
+ return;
1963
+ initialized = true;
1964
+ const parsed = parsePatterns(process.env.DEBUG);
1965
+ patterns = parsed.length > 0 ? parsed : null;
1966
+ }
1967
+ function getOrCreateFileSink(path) {
1968
+ let sink = fileSinkCache.get(path);
1969
+ if (!sink) {
1970
+ sink = new FileSink(path);
1971
+ fileSinkCache.set(path, sink);
1972
+ }
1973
+ return sink;
1974
+ }
1975
+ return {
1976
+ enableFileLogs(scope, sinkForTesting) {
1977
+ const existing = process.env[DUST_LOG_FILE];
1978
+ const logDir = process.env.DUST_LOG_DIR ?? join6(process.cwd(), "log");
1979
+ const path = existing ?? join6(logDir, `${scope}.log`);
1980
+ if (!existing) {
1981
+ process.env[DUST_LOG_FILE] = path;
1982
+ }
1983
+ activeFileSink = sinkForTesting ?? new FileSink(path);
1984
+ },
1985
+ createLogger(name, options) {
1986
+ let perLoggerSink;
1987
+ if (options?.file === false) {
1988
+ perLoggerSink = null;
1989
+ } else if (typeof options?.file === "string") {
1990
+ perLoggerSink = getOrCreateFileSink(options.file);
1991
+ }
1992
+ return (...messages) => {
1993
+ init();
1994
+ const line = formatLine(name, messages);
1995
+ if (perLoggerSink !== undefined) {
1996
+ if (perLoggerSink !== null) {
1997
+ perLoggerSink.write(line);
1998
+ }
1999
+ } else if (activeFileSink) {
2000
+ activeFileSink.write(line);
2001
+ }
2002
+ if (patterns && matchesAny(name, patterns)) {
2003
+ process.stdout.write(line);
2004
+ }
2005
+ };
2006
+ },
2007
+ isEnabled(name) {
2008
+ init();
2009
+ return patterns !== null && matchesAny(name, patterns);
2010
+ }
2011
+ };
2012
+ }
2013
+ var defaultService = createLoggingService();
2014
+ var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
2015
+ var createLogger = defaultService.createLogger.bind(defaultService);
2016
+ var isEnabled = defaultService.isEnabled.bind(defaultService);
2017
+
2018
+ // lib/claude/spawn-claude-code.ts
2019
+ var debug = createLogger("dust.claude.spawn-claude-code");
1780
2020
  var defaultDependencies = {
1781
2021
  spawn: nodeSpawn2,
1782
2022
  createInterface: nodeCreateInterface
@@ -1859,7 +2099,9 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
1859
2099
  continue;
1860
2100
  try {
1861
2101
  yield JSON.parse(line);
1862
- } catch {}
2102
+ } catch {
2103
+ debug("Skipping malformed JSON line: %s", line.slice(0, 200));
2104
+ }
1863
2105
  }
1864
2106
  await closePromise;
1865
2107
  } finally {
@@ -2190,131 +2432,6 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
2190
2432
  await dependencies.streamEvents(events, sink, onRawEvent);
2191
2433
  }
2192
2434
 
2193
- // lib/logging/index.ts
2194
- import { join as join6 } from "node:path";
2195
-
2196
- // lib/logging/match.ts
2197
- function parsePatterns(debug) {
2198
- if (!debug)
2199
- return [];
2200
- const expressions = debug.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2201
- return expressions.map((expr) => {
2202
- const escaped = expr.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2203
- const pattern = escaped.replace(/\*/g, ".*");
2204
- return new RegExp(`^${pattern}$`);
2205
- });
2206
- }
2207
- function matchesAny(name, patterns) {
2208
- return patterns.some((re) => re.test(name));
2209
- }
2210
- function formatLine(name, messages) {
2211
- const text = messages.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
2212
- return `${new Date().toISOString()} [${name}] ${text}
2213
- `;
2214
- }
2215
-
2216
- // lib/logging/sink.ts
2217
- import { appendFileSync, mkdirSync } from "node:fs";
2218
- import { dirname } from "node:path";
2219
-
2220
- class FileSink {
2221
- logPath;
2222
- _appendFileSync;
2223
- _mkdirSync;
2224
- resolvedPath;
2225
- ready = false;
2226
- constructor(logPath, _appendFileSync = appendFileSync, _mkdirSync = mkdirSync) {
2227
- this.logPath = logPath;
2228
- this._appendFileSync = _appendFileSync;
2229
- this._mkdirSync = _mkdirSync;
2230
- }
2231
- ensureLogFile() {
2232
- if (this.ready)
2233
- return this.resolvedPath;
2234
- this.ready = true;
2235
- this.resolvedPath = this.logPath;
2236
- try {
2237
- this._mkdirSync(dirname(this.logPath), { recursive: true });
2238
- } catch {
2239
- this.resolvedPath = undefined;
2240
- }
2241
- return this.resolvedPath;
2242
- }
2243
- write(line) {
2244
- const path = this.ensureLogFile();
2245
- if (!path)
2246
- return;
2247
- try {
2248
- this._appendFileSync(path, line);
2249
- } catch {}
2250
- }
2251
- }
2252
-
2253
- // lib/logging/index.ts
2254
- var DUST_LOG_FILE = "DUST_LOG_FILE";
2255
- function createLoggingService() {
2256
- let patterns = null;
2257
- let initialized = false;
2258
- let activeFileSink = null;
2259
- const fileSinkCache = new Map;
2260
- function init() {
2261
- if (initialized)
2262
- return;
2263
- initialized = true;
2264
- const parsed = parsePatterns(process.env.DEBUG);
2265
- patterns = parsed.length > 0 ? parsed : null;
2266
- }
2267
- function getOrCreateFileSink(path) {
2268
- let sink = fileSinkCache.get(path);
2269
- if (!sink) {
2270
- sink = new FileSink(path);
2271
- fileSinkCache.set(path, sink);
2272
- }
2273
- return sink;
2274
- }
2275
- return {
2276
- enableFileLogs(scope, sinkForTesting) {
2277
- const existing = process.env[DUST_LOG_FILE];
2278
- const logDir = process.env.DUST_LOG_DIR ?? join6(process.cwd(), "log");
2279
- const path = existing ?? join6(logDir, `${scope}.log`);
2280
- if (!existing) {
2281
- process.env[DUST_LOG_FILE] = path;
2282
- }
2283
- activeFileSink = sinkForTesting ?? new FileSink(path);
2284
- },
2285
- createLogger(name, options) {
2286
- let perLoggerSink;
2287
- if (options?.file === false) {
2288
- perLoggerSink = null;
2289
- } else if (typeof options?.file === "string") {
2290
- perLoggerSink = getOrCreateFileSink(options.file);
2291
- }
2292
- return (...messages) => {
2293
- init();
2294
- const line = formatLine(name, messages);
2295
- if (perLoggerSink !== undefined) {
2296
- if (perLoggerSink !== null) {
2297
- perLoggerSink.write(line);
2298
- }
2299
- } else if (activeFileSink) {
2300
- activeFileSink.write(line);
2301
- }
2302
- if (patterns && matchesAny(name, patterns)) {
2303
- process.stdout.write(line);
2304
- }
2305
- };
2306
- },
2307
- isEnabled(name) {
2308
- init();
2309
- return patterns !== null && matchesAny(name, patterns);
2310
- }
2311
- };
2312
- }
2313
- var defaultService = createLoggingService();
2314
- var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
2315
- var createLogger = defaultService.createLogger.bind(defaultService);
2316
- var isEnabled = defaultService.isEnabled.bind(defaultService);
2317
-
2318
2435
  // lib/bucket/repository-git.ts
2319
2436
  import { join as join7 } from "node:path";
2320
2437
  function getRepoPath(repoName, reposDir) {
@@ -2402,11 +2519,15 @@ function titleToFilename(title) {
2402
2519
  }
2403
2520
 
2404
2521
  // lib/cli/commands/focus.ts
2405
- function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2522
+ function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPath, installCommand) {
2406
2523
  const steps = [];
2407
2524
  let step = 1;
2408
2525
  const hasIdeaFile = !taskTitle?.startsWith(EXPEDITE_IDEA_PREFIX);
2409
2526
  steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
2527
+ if (installCommand) {
2528
+ steps.push(`${step}. Run \`${installCommand}\` to install dependencies`);
2529
+ step++;
2530
+ }
2410
2531
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
2411
2532
  step++;
2412
2533
  steps.push(`${step}. Implement the task`);
@@ -2416,10 +2537,11 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2416
2537
  step++;
2417
2538
  }
2418
2539
  const commitMessageLine = taskTitle ? ` Use this exact commit message: "${taskTitle}". Do not add any prefix.` : ' Use the task title as the commit message. Do not add prefixes like "Complete task:" - use the title directly.';
2540
+ const deleteTaskLine = taskPath ? ` - Deletion of the completed task file (\`${taskPath}\`)` : " - Deletion of the completed task file";
2419
2541
  const commitItems = [
2420
2542
  " - All implementation changes",
2421
- " - Deletion of the completed task file",
2422
- " - Updates to any facts that changed"
2543
+ deleteTaskLine,
2544
+ ` - Updates to any facts that changed (run \`${bin} facts\` if needed)`
2423
2545
  ];
2424
2546
  if (hasIdeaFile) {
2425
2547
  commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
@@ -2725,18 +2847,14 @@ Make sure the repository is in a clean state and synced with remote before finis
2725
2847
  onLoopEvent({ type: "loop.tasks_found" });
2726
2848
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
2727
2849
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
2728
- const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined);
2729
- const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
2730
-
2731
- The following is the contents of the task file \`${task.path}\`:
2850
+ const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined, task.path, installCommand);
2851
+ const prompt = `Implement the task at \`${task.path}\`:
2732
2852
 
2733
2853
  ----------
2734
2854
  ${taskContent}
2735
2855
  ----------
2736
2856
 
2737
- When the task is complete, delete the task file \`${task.path}\`.
2738
-
2739
- ## Instructions
2857
+ ## How to implement the task
2740
2858
 
2741
2859
  ${instructions}`;
2742
2860
  onAgentEvent?.({
@@ -2877,6 +2995,9 @@ function buildEventMessage(parameters) {
2877
2995
  repository: parameters.repository,
2878
2996
  event: parameters.event
2879
2997
  };
2998
+ if (parameters.repoId !== undefined) {
2999
+ msg.repoId = parameters.repoId;
3000
+ }
2880
3001
  if (parameters.agentSessionId) {
2881
3002
  msg.agentSessionId = parameters.agentSessionId;
2882
3003
  }
@@ -2962,6 +3083,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2962
3083
  sequence,
2963
3084
  sessionId,
2964
3085
  repository: repoName,
3086
+ repoId: repoState.repository.id,
2965
3087
  event,
2966
3088
  agentSessionId
2967
3089
  }));
@@ -3965,7 +4087,12 @@ async function shutdown(state, bucketDeps, context) {
3965
4087
  repoState.wakeUp?.();
3966
4088
  }
3967
4089
  const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
3968
- await Promise.all(loopPromises.map((p) => p.catch(() => {})));
4090
+ const results = await Promise.allSettled(loopPromises);
4091
+ for (const result of results) {
4092
+ if (result.status === "rejected") {
4093
+ context.stderr(`Repository loop failed: ${result.reason}`);
4094
+ }
4095
+ }
3969
4096
  for (const repoState of state.repositories.values()) {
3970
4097
  await removeRepository(repoState.path, bucketDeps.spawn, context);
3971
4098
  }
@@ -5624,6 +5751,7 @@ async function list(dependencies) {
5624
5751
  // lib/codex/spawn-codex.ts
5625
5752
  import { spawn as nodeSpawn5 } from "node:child_process";
5626
5753
  import { createInterface as nodeCreateInterface2 } from "node:readline";
5754
+ var debug2 = createLogger("dust.codex.spawn-codex");
5627
5755
  var defaultDependencies2 = {
5628
5756
  spawn: nodeSpawn5,
5629
5757
  createInterface: nodeCreateInterface2
@@ -5673,7 +5801,9 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
5673
5801
  continue;
5674
5802
  try {
5675
5803
  yield JSON.parse(line);
5676
- } catch {}
5804
+ } catch {
5805
+ debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
5806
+ }
5677
5807
  }
5678
5808
  await closePromise;
5679
5809
  } finally {
package/dist/types.d.ts CHANGED
@@ -6,5 +6,7 @@
6
6
  */
7
7
  export type { AgentSessionEvent, EventMessage } from './agent-events';
8
8
  export type { Idea, IdeaOpenQuestion, IdeaOption } from './artifacts/ideas';
9
+ export type { TaskGraph, TaskGraphNode } from './artifacts/index';
10
+ export type { Task } from './artifacts/tasks';
9
11
  export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './artifacts/workflow-tasks';
10
12
  export type { Repository } from './bucket/repository';
@@ -730,7 +730,11 @@ function createOverlayFileSystem(base, patchFiles, deletedPaths = new Set) {
730
730
  entries.add(entry);
731
731
  }
732
732
  }
733
- } catch {}
733
+ } catch (error) {
734
+ if (error.code !== "ENOENT") {
735
+ throw error;
736
+ }
737
+ }
734
738
  return Array.from(entries);
735
739
  },
736
740
  isDirectory(path) {
@@ -821,7 +825,11 @@ async function validatePatch(fileSystem, dustPath, patch) {
821
825
  const content = await overlayFs.readFile(filePath);
822
826
  allRelationships.push(extractPrincipleRelationships(filePath, content));
823
827
  }
824
- } catch {}
828
+ } catch (error) {
829
+ if (error.code !== "ENOENT") {
830
+ throw error;
831
+ }
832
+ }
825
833
  violations.push(...validateBidirectionalLinks(allRelationships));
826
834
  violations.push(...validateNoCycles(allRelationships));
827
835
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {