@joshski/dust 0.1.77 → 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.
@@ -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
  }
@@ -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.77";
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) {
@@ -1521,11 +1555,11 @@ var NO_COLORS = {
1521
1555
  green: "",
1522
1556
  yellow: ""
1523
1557
  };
1524
- function shouldDisableColors() {
1525
- if (process.env.NO_COLOR !== undefined) {
1558
+ function shouldDisableColors(env = process.env) {
1559
+ if (env.NO_COLOR !== undefined) {
1526
1560
  return true;
1527
1561
  }
1528
- if (process.env.TERM === "dumb") {
1562
+ if (env.TERM === "dumb") {
1529
1563
  return true;
1530
1564
  }
1531
1565
  if (!process.stdout.isTTY) {
@@ -1641,8 +1675,8 @@ var CREDENTIALS_DIR = ".dust";
1641
1675
  var CREDENTIALS_FILE = "credentials.json";
1642
1676
  var AUTH_TIMEOUT_MS = 120000;
1643
1677
  var DEFAULT_DUSTBUCKET_HOST = "https://dustbucket.com";
1644
- function getDustbucketHost() {
1645
- return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
1678
+ function getDustbucketHost(env = process.env) {
1679
+ return env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
1646
1680
  }
1647
1681
  function credentialsPath(homeDir) {
1648
1682
  return join4(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
@@ -1653,8 +1687,11 @@ async function loadStoredToken(fileSystem, homeDir) {
1653
1687
  const content = await fileSystem.readFile(path);
1654
1688
  const data = JSON.parse(content);
1655
1689
  return typeof data.token === "string" ? data.token : null;
1656
- } catch {
1657
- return null;
1690
+ } catch (error) {
1691
+ if (error.code === "ENOENT") {
1692
+ return null;
1693
+ }
1694
+ throw error;
1658
1695
  }
1659
1696
  }
1660
1697
  async function storeToken(fileSystem, homeDir, token) {
@@ -1666,7 +1703,12 @@ async function clearToken(fileSystem, homeDir) {
1666
1703
  const path = credentialsPath(homeDir);
1667
1704
  try {
1668
1705
  await fileSystem.writeFile(path, "{}");
1669
- } catch {}
1706
+ } catch (error) {
1707
+ if (error.code === "ENOENT") {
1708
+ return;
1709
+ }
1710
+ throw error;
1711
+ }
1670
1712
  }
1671
1713
  async function defaultExchangeCode(code) {
1672
1714
  const host = getDustbucketHost();
@@ -1847,6 +1889,134 @@ import { dirname as dirname2 } from "node:path";
1847
1889
  // lib/claude/spawn-claude-code.ts
1848
1890
  import { spawn as nodeSpawn2 } from "node:child_process";
1849
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");
1850
2020
  var defaultDependencies = {
1851
2021
  spawn: nodeSpawn2,
1852
2022
  createInterface: nodeCreateInterface
@@ -1929,7 +2099,9 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
1929
2099
  continue;
1930
2100
  try {
1931
2101
  yield JSON.parse(line);
1932
- } catch {}
2102
+ } catch {
2103
+ debug("Skipping malformed JSON line: %s", line.slice(0, 200));
2104
+ }
1933
2105
  }
1934
2106
  await closePromise;
1935
2107
  } finally {
@@ -2260,131 +2432,6 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
2260
2432
  await dependencies.streamEvents(events, sink, onRawEvent);
2261
2433
  }
2262
2434
 
2263
- // lib/logging/index.ts
2264
- import { join as join6 } from "node:path";
2265
-
2266
- // lib/logging/match.ts
2267
- function parsePatterns(debug) {
2268
- if (!debug)
2269
- return [];
2270
- const expressions = debug.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
2271
- return expressions.map((expr) => {
2272
- const escaped = expr.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2273
- const pattern = escaped.replace(/\*/g, ".*");
2274
- return new RegExp(`^${pattern}$`);
2275
- });
2276
- }
2277
- function matchesAny(name, patterns) {
2278
- return patterns.some((re) => re.test(name));
2279
- }
2280
- function formatLine(name, messages) {
2281
- const text = messages.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
2282
- return `${new Date().toISOString()} [${name}] ${text}
2283
- `;
2284
- }
2285
-
2286
- // lib/logging/sink.ts
2287
- import { appendFileSync, mkdirSync } from "node:fs";
2288
- import { dirname } from "node:path";
2289
-
2290
- class FileSink {
2291
- logPath;
2292
- _appendFileSync;
2293
- _mkdirSync;
2294
- resolvedPath;
2295
- ready = false;
2296
- constructor(logPath, _appendFileSync = appendFileSync, _mkdirSync = mkdirSync) {
2297
- this.logPath = logPath;
2298
- this._appendFileSync = _appendFileSync;
2299
- this._mkdirSync = _mkdirSync;
2300
- }
2301
- ensureLogFile() {
2302
- if (this.ready)
2303
- return this.resolvedPath;
2304
- this.ready = true;
2305
- this.resolvedPath = this.logPath;
2306
- try {
2307
- this._mkdirSync(dirname(this.logPath), { recursive: true });
2308
- } catch {
2309
- this.resolvedPath = undefined;
2310
- }
2311
- return this.resolvedPath;
2312
- }
2313
- write(line) {
2314
- const path = this.ensureLogFile();
2315
- if (!path)
2316
- return;
2317
- try {
2318
- this._appendFileSync(path, line);
2319
- } catch {}
2320
- }
2321
- }
2322
-
2323
- // lib/logging/index.ts
2324
- var DUST_LOG_FILE = "DUST_LOG_FILE";
2325
- function createLoggingService() {
2326
- let patterns = null;
2327
- let initialized = false;
2328
- let activeFileSink = null;
2329
- const fileSinkCache = new Map;
2330
- function init() {
2331
- if (initialized)
2332
- return;
2333
- initialized = true;
2334
- const parsed = parsePatterns(process.env.DEBUG);
2335
- patterns = parsed.length > 0 ? parsed : null;
2336
- }
2337
- function getOrCreateFileSink(path) {
2338
- let sink = fileSinkCache.get(path);
2339
- if (!sink) {
2340
- sink = new FileSink(path);
2341
- fileSinkCache.set(path, sink);
2342
- }
2343
- return sink;
2344
- }
2345
- return {
2346
- enableFileLogs(scope, sinkForTesting) {
2347
- const existing = process.env[DUST_LOG_FILE];
2348
- const logDir = process.env.DUST_LOG_DIR ?? join6(process.cwd(), "log");
2349
- const path = existing ?? join6(logDir, `${scope}.log`);
2350
- if (!existing) {
2351
- process.env[DUST_LOG_FILE] = path;
2352
- }
2353
- activeFileSink = sinkForTesting ?? new FileSink(path);
2354
- },
2355
- createLogger(name, options) {
2356
- let perLoggerSink;
2357
- if (options?.file === false) {
2358
- perLoggerSink = null;
2359
- } else if (typeof options?.file === "string") {
2360
- perLoggerSink = getOrCreateFileSink(options.file);
2361
- }
2362
- return (...messages) => {
2363
- init();
2364
- const line = formatLine(name, messages);
2365
- if (perLoggerSink !== undefined) {
2366
- if (perLoggerSink !== null) {
2367
- perLoggerSink.write(line);
2368
- }
2369
- } else if (activeFileSink) {
2370
- activeFileSink.write(line);
2371
- }
2372
- if (patterns && matchesAny(name, patterns)) {
2373
- process.stdout.write(line);
2374
- }
2375
- };
2376
- },
2377
- isEnabled(name) {
2378
- init();
2379
- return patterns !== null && matchesAny(name, patterns);
2380
- }
2381
- };
2382
- }
2383
- var defaultService = createLoggingService();
2384
- var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
2385
- var createLogger = defaultService.createLogger.bind(defaultService);
2386
- var isEnabled = defaultService.isEnabled.bind(defaultService);
2387
-
2388
2435
  // lib/bucket/repository-git.ts
2389
2436
  import { join as join7 } from "node:path";
2390
2437
  function getRepoPath(repoName, reposDir) {
@@ -2472,11 +2519,15 @@ function titleToFilename(title) {
2472
2519
  }
2473
2520
 
2474
2521
  // lib/cli/commands/focus.ts
2475
- function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2522
+ function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPath, installCommand) {
2476
2523
  const steps = [];
2477
2524
  let step = 1;
2478
2525
  const hasIdeaFile = !taskTitle?.startsWith(EXPEDITE_IDEA_PREFIX);
2479
2526
  steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
2527
+ if (installCommand) {
2528
+ steps.push(`${step}. Run \`${installCommand}\` to install dependencies`);
2529
+ step++;
2530
+ }
2480
2531
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
2481
2532
  step++;
2482
2533
  steps.push(`${step}. Implement the task`);
@@ -2486,10 +2537,11 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2486
2537
  step++;
2487
2538
  }
2488
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";
2489
2541
  const commitItems = [
2490
2542
  " - All implementation changes",
2491
- " - Deletion of the completed task file",
2492
- " - Updates to any facts that changed"
2543
+ deleteTaskLine,
2544
+ ` - Updates to any facts that changed (run \`${bin} facts\` if needed)`
2493
2545
  ];
2494
2546
  if (hasIdeaFile) {
2495
2547
  commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
@@ -2795,18 +2847,14 @@ Make sure the repository is in a clean state and synced with remote before finis
2795
2847
  onLoopEvent({ type: "loop.tasks_found" });
2796
2848
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
2797
2849
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
2798
- const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined);
2799
- const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
2800
-
2801
- 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}\`:
2802
2852
 
2803
2853
  ----------
2804
2854
  ${taskContent}
2805
2855
  ----------
2806
2856
 
2807
- When the task is complete, delete the task file \`${task.path}\`.
2808
-
2809
- ## Instructions
2857
+ ## How to implement the task
2810
2858
 
2811
2859
  ${instructions}`;
2812
2860
  onAgentEvent?.({
@@ -4039,7 +4087,12 @@ async function shutdown(state, bucketDeps, context) {
4039
4087
  repoState.wakeUp?.();
4040
4088
  }
4041
4089
  const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
4042
- 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
+ }
4043
4096
  for (const repoState of state.repositories.values()) {
4044
4097
  await removeRepository(repoState.path, bucketDeps.spawn, context);
4045
4098
  }
@@ -5698,6 +5751,7 @@ async function list(dependencies) {
5698
5751
  // lib/codex/spawn-codex.ts
5699
5752
  import { spawn as nodeSpawn5 } from "node:child_process";
5700
5753
  import { createInterface as nodeCreateInterface2 } from "node:readline";
5754
+ var debug2 = createLogger("dust.codex.spawn-codex");
5701
5755
  var defaultDependencies2 = {
5702
5756
  spawn: nodeSpawn5,
5703
5757
  createInterface: nodeCreateInterface2
@@ -5747,7 +5801,9 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
5747
5801
  continue;
5748
5802
  try {
5749
5803
  yield JSON.parse(line);
5750
- } catch {}
5804
+ } catch {
5805
+ debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
5806
+ }
5751
5807
  }
5752
5808
  await closePromise;
5753
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.77",
3
+ "version": "0.1.78",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {