@joshski/dust 0.1.77 → 0.1.79

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.
@@ -40,4 +40,15 @@ export interface EventMessage {
40
40
  * is forwarded as a claude-event.
41
41
  */
42
42
  export declare function rawEventToAgentEvent(rawEvent: Record<string, unknown>): AgentSessionEvent;
43
+ /**
44
+ * Create a heartbeat throttler that limits agent-session-activity events
45
+ * to at most once per interval (default: 5 seconds).
46
+ *
47
+ * The returned callback converts raw Claude events to AgentSessionEvents,
48
+ * throttling stream_event heartbeats while forwarding all other events.
49
+ */
50
+ export declare function createHeartbeatThrottler(onAgentEvent: (event: AgentSessionEvent) => void, options?: {
51
+ intervalMs?: number;
52
+ now?: () => number;
53
+ }): (rawEvent: Record<string, unknown>) => void;
43
54
  export declare function formatAgentEvent(event: AgentSessionEvent): string | null;
@@ -3,11 +3,24 @@ 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 };
21
+ export type ArtifactType = 'ideas' | 'tasks' | 'principles' | 'facts';
10
22
  export interface ArtifactsRepository {
23
+ artifactPath(type: ArtifactType, slug: string): string;
11
24
  parseIdea(options: {
12
25
  slug: string;
13
26
  }): Promise<Idea>;
@@ -49,6 +62,7 @@ export interface ArtifactsRepository {
49
62
  parseCaptureIdeaTask(options: {
50
63
  taskSlug: string;
51
64
  }): Promise<ParsedCaptureIdeaTask | null>;
65
+ buildTaskGraph(): Promise<TaskGraph>;
52
66
  }
53
67
  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'>;
68
+ export declare function buildReadOnlyArtifactsRepository(fileSystem: ReadableFileSystem, dustPath: string): Pick<ArtifactsRepository, 'artifactPath' | 'parseIdea' | 'listIdeas' | 'parsePrinciple' | 'listPrinciples' | 'parseFact' | 'listFacts' | 'parseTask' | 'listTasks' | 'findWorkflowTaskForIdea' | 'parseCaptureIdeaTask' | 'buildTaskGraph'>;
package/dist/artifacts.js CHANGED
@@ -548,6 +548,9 @@ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
548
548
  // lib/artifacts/index.ts
549
549
  function buildArtifactsRepository(fileSystem, dustPath) {
550
550
  return {
551
+ artifactPath(type, slug) {
552
+ return `${dustPath}/${type}/${slug}.md`;
553
+ },
551
554
  async parseIdea(options) {
552
555
  return parseIdea(fileSystem, dustPath, options.slug);
553
556
  },
@@ -609,11 +612,35 @@ function buildArtifactsRepository(fileSystem, dustPath) {
609
612
  },
610
613
  async parseCaptureIdeaTask(options) {
611
614
  return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
615
+ },
616
+ async buildTaskGraph() {
617
+ const taskSlugs = await this.listTasks();
618
+ const allWorkflowTasks = await findAllWorkflowTasks(fileSystem, dustPath);
619
+ const workflowTypeByTaskSlug = new Map;
620
+ for (const match of allWorkflowTasks.workflowTasksByIdeaSlug.values()) {
621
+ workflowTypeByTaskSlug.set(match.taskSlug, match.type);
622
+ }
623
+ const nodes = [];
624
+ const edges = [];
625
+ for (const slug of taskSlugs) {
626
+ const task = await this.parseTask({ slug });
627
+ nodes.push({
628
+ task,
629
+ workflowType: workflowTypeByTaskSlug.get(slug) ?? null
630
+ });
631
+ for (const blockerSlug of task.blockedBy) {
632
+ edges.push({ from: blockerSlug, to: slug });
633
+ }
634
+ }
635
+ return { nodes, edges };
612
636
  }
613
637
  };
614
638
  }
615
639
  function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
616
640
  return {
641
+ artifactPath(type, slug) {
642
+ return `${dustPath}/${type}/${slug}.md`;
643
+ },
617
644
  async parseIdea(options) {
618
645
  return parseIdea(fileSystem, dustPath, options.slug);
619
646
  },
@@ -663,6 +690,27 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
663
690
  },
664
691
  async parseCaptureIdeaTask(options) {
665
692
  return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
693
+ },
694
+ async buildTaskGraph() {
695
+ const taskSlugs = await this.listTasks();
696
+ const allWorkflowTasks = await findAllWorkflowTasks(fileSystem, dustPath);
697
+ const workflowTypeByTaskSlug = new Map;
698
+ for (const match of allWorkflowTasks.workflowTasksByIdeaSlug.values()) {
699
+ workflowTypeByTaskSlug.set(match.taskSlug, match.type);
700
+ }
701
+ const nodes = [];
702
+ const edges = [];
703
+ for (const slug of taskSlugs) {
704
+ const task = await this.parseTask({ slug });
705
+ nodes.push({
706
+ task,
707
+ workflowType: workflowTypeByTaskSlug.get(slug) ?? null
708
+ });
709
+ for (const blockerSlug of task.blockedBy) {
710
+ edges.push({ from: blockerSlug, to: slug });
711
+ }
712
+ }
713
+ return { nodes, edges };
666
714
  }
667
715
  };
668
716
  }
@@ -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.79";
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) {
@@ -2437,6 +2484,23 @@ function rawEventToAgentEvent(rawEvent) {
2437
2484
  }
2438
2485
  return { type: "claude-event", rawEvent };
2439
2486
  }
2487
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 5000;
2488
+ function createHeartbeatThrottler(onAgentEvent, options) {
2489
+ const intervalMs = options?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
2490
+ const now = options?.now ?? Date.now;
2491
+ let lastHeartbeatTime;
2492
+ return (rawEvent) => {
2493
+ const event = rawEventToAgentEvent(rawEvent);
2494
+ if (event.type === "agent-session-activity") {
2495
+ const currentTime = now();
2496
+ if (lastHeartbeatTime !== undefined && currentTime - lastHeartbeatTime < intervalMs) {
2497
+ return;
2498
+ }
2499
+ lastHeartbeatTime = currentTime;
2500
+ }
2501
+ onAgentEvent(event);
2502
+ };
2503
+ }
2440
2504
  function agentDisplayName(agentType) {
2441
2505
  if (agentType === "codex")
2442
2506
  return "Codex";
@@ -2472,11 +2536,15 @@ function titleToFilename(title) {
2472
2536
  }
2473
2537
 
2474
2538
  // lib/cli/commands/focus.ts
2475
- function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2539
+ function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPath, installCommand) {
2476
2540
  const steps = [];
2477
2541
  let step = 1;
2478
2542
  const hasIdeaFile = !taskTitle?.startsWith(EXPEDITE_IDEA_PREFIX);
2479
2543
  steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
2544
+ if (installCommand) {
2545
+ steps.push(`${step}. Run \`${installCommand}\` to install dependencies`);
2546
+ step++;
2547
+ }
2480
2548
  steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
2481
2549
  step++;
2482
2550
  steps.push(`${step}. Implement the task`);
@@ -2486,10 +2554,11 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
2486
2554
  step++;
2487
2555
  }
2488
2556
  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.';
2557
+ const deleteTaskLine = taskPath ? ` - Deletion of the completed task file (\`${taskPath}\`)` : " - Deletion of the completed task file";
2489
2558
  const commitItems = [
2490
2559
  " - All implementation changes",
2491
- " - Deletion of the completed task file",
2492
- " - Updates to any facts that changed"
2560
+ deleteTaskLine,
2561
+ ` - Updates to any facts that changed (run \`${bin} facts\` if needed)`
2493
2562
  ];
2494
2563
  if (hasIdeaFile) {
2495
2564
  commitItems.push(" - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)");
@@ -2795,18 +2864,14 @@ Make sure the repository is in a clean state and synced with remote before finis
2795
2864
  onLoopEvent({ type: "loop.tasks_found" });
2796
2865
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
2797
2866
  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}\`:
2867
+ const instructions = buildImplementationInstructions(dustCommand, hooksInstalled, task.title ?? undefined, task.path, installCommand);
2868
+ const prompt = `Implement the task at \`${task.path}\`:
2802
2869
 
2803
2870
  ----------
2804
2871
  ${taskContent}
2805
2872
  ----------
2806
2873
 
2807
- When the task is complete, delete the task file \`${task.path}\`.
2808
-
2809
- ## Instructions
2874
+ ## How to implement the task
2810
2875
 
2811
2876
  ${instructions}`;
2812
2877
  onAgentEvent?.({
@@ -2894,9 +2959,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2894
2959
  let completedIterations = 0;
2895
2960
  const iterationOptions = { hooksInstalled };
2896
2961
  if (eventsUrl) {
2897
- iterationOptions.onRawEvent = (rawEvent) => {
2898
- onAgentEvent(rawEventToAgentEvent(rawEvent));
2899
- };
2962
+ iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
2900
2963
  }
2901
2964
  while (completedIterations < maxIterations) {
2902
2965
  agentSessionId = crypto.randomUUID();
@@ -3057,9 +3120,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3057
3120
  hooksInstalled,
3058
3121
  signal: abortController.signal,
3059
3122
  repositoryId: repoState.repository.id.toString(),
3060
- onRawEvent: (rawEvent) => {
3061
- onAgentEvent(rawEventToAgentEvent(rawEvent));
3062
- }
3123
+ onRawEvent: createHeartbeatThrottler(onAgentEvent)
3063
3124
  });
3064
3125
  } catch (error) {
3065
3126
  const msg = error instanceof Error ? error.message : String(error);
@@ -4039,7 +4100,12 @@ async function shutdown(state, bucketDeps, context) {
4039
4100
  repoState.wakeUp?.();
4040
4101
  }
4041
4102
  const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
4042
- await Promise.all(loopPromises.map((p) => p.catch(() => {})));
4103
+ const results = await Promise.allSettled(loopPromises);
4104
+ for (const result of results) {
4105
+ if (result.status === "rejected") {
4106
+ context.stderr(`Repository loop failed: ${result.reason}`);
4107
+ }
4108
+ }
4043
4109
  for (const repoState of state.repositories.values()) {
4044
4110
  await removeRepository(repoState.path, bucketDeps.spawn, context);
4045
4111
  }
@@ -5698,6 +5764,7 @@ async function list(dependencies) {
5698
5764
  // lib/codex/spawn-codex.ts
5699
5765
  import { spawn as nodeSpawn5 } from "node:child_process";
5700
5766
  import { createInterface as nodeCreateInterface2 } from "node:readline";
5767
+ var debug2 = createLogger("dust.codex.spawn-codex");
5701
5768
  var defaultDependencies2 = {
5702
5769
  spawn: nodeSpawn5,
5703
5770
  createInterface: nodeCreateInterface2
@@ -5747,7 +5814,9 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
5747
5814
  continue;
5748
5815
  try {
5749
5816
  yield JSON.parse(line);
5750
- } catch {}
5817
+ } catch {
5818
+ debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
5819
+ }
5751
5820
  }
5752
5821
  await closePromise;
5753
5822
  } 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 { ArtifactType, 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.79",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {