@joshski/dust 0.1.85 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/artifacts.js CHANGED
@@ -276,6 +276,18 @@ var EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
276
276
  function titleToFilename(title) {
277
277
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
278
278
  }
279
+ var WORKFLOW_HINT_PATHS = {
280
+ refine: "config/workflow-hints/refine.md",
281
+ "decompose-idea": "config/workflow-hints/decompose-idea.md",
282
+ shelve: "config/workflow-hints/shelve.md"
283
+ };
284
+ async function readWorkflowHint(fileSystem, dustPath, workflowType) {
285
+ const hintPath = `${dustPath}/${WORKFLOW_HINT_PATHS[workflowType]}`;
286
+ if (!fileSystem.exists(hintPath)) {
287
+ return null;
288
+ }
289
+ return fileSystem.readFile(hintPath);
290
+ }
279
291
  var WORKFLOW_SECTION_HEADINGS = [
280
292
  { type: "refine", heading: "Refines Idea" },
281
293
  { type: "decompose-idea", heading: "Decomposes Idea" },
@@ -418,12 +430,16 @@ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
418
430
  `)}
419
431
  `;
420
432
  }
421
- async function createIdeaTransitionTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, ideaSectionHeading, taskOptions) {
433
+ async function createIdeaTransitionTask(fileSystem, dustPath, workflowType, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, ideaSectionHeading, taskOptions) {
422
434
  const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
423
435
  const taskTitle = `${prefix}${ideaTitle}`;
424
436
  const filename = titleToFilename(taskTitle);
425
437
  const filePath = `${dustPath}/tasks/${filename}`;
426
- const openingSentence = openingSentenceTemplate(ideaTitle);
438
+ const baseOpeningSentence = openingSentenceTemplate(ideaTitle);
439
+ const hint = await readWorkflowHint(fileSystem, dustPath, workflowType);
440
+ const openingSentence = hint ? `${baseOpeningSentence}
441
+
442
+ ${hint}` : baseOpeningSentence;
427
443
  const ideaSection = { heading: ideaSectionHeading, ideaTitle, ideaSlug };
428
444
  const content = renderTask(taskTitle, openingSentence, definitionOfDone, ideaSection, {
429
445
  description: taskOptions?.description,
@@ -434,7 +450,7 @@ async function createIdeaTransitionTask(fileSystem, dustPath, prefix, ideaSlug,
434
450
  }
435
451
  async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description, dustCommand) {
436
452
  const cmd = dustCommand ?? "dust";
437
- return createIdeaTransitionTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Run \`${cmd} principles\` for alignment and \`${cmd} facts\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
453
+ return createIdeaTransitionTask(fileSystem, dustPath, "refine", "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Run \`${cmd} principles\` for alignment and \`${cmd} facts\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking.`, [
438
454
  "Idea is thoroughly researched with relevant codebase context",
439
455
  "Open questions are added for any ambiguous or underspecified aspects",
440
456
  "Open questions follow the required heading format and focus on high-value decisions",
@@ -443,7 +459,7 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description,
443
459
  }
444
460
  async function decomposeIdea(fileSystem, dustPath, options, dustCommand) {
445
461
  const cmd = dustCommand ?? "dust";
446
- return createIdeaTransitionTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Run \`${cmd} principles\` to link relevant principles and \`${cmd} facts\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
462
+ return createIdeaTransitionTask(fileSystem, dustPath, "decompose-idea", "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Run \`${cmd} principles\` to link relevant principles and \`${cmd} facts\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
447
463
  "One or more new tasks are created in .dust/tasks/",
448
464
  "Task's Principles section links to relevant principles from .dust/principles/",
449
465
  "The original idea is deleted or updated to reflect remaining scope"
@@ -453,7 +469,7 @@ async function decomposeIdea(fileSystem, dustPath, options, dustCommand) {
453
469
  });
454
470
  }
455
471
  async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description, _dustCommand) {
456
- return createIdeaTransitionTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], "Shelves Idea", { description });
472
+ return createIdeaTransitionTask(fileSystem, dustPath, "shelve", "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], "Shelves Idea", { description });
457
473
  }
458
474
  async function createIdeaTask(fileSystem, dustPath, options) {
459
475
  const { title, description, expedite, dustCommand } = options;
@@ -8,6 +8,7 @@ import type { AgentSessionEvent, EventMessage } from '../agent-events';
8
8
  import { type run as claudeRun, type RunnerDependencies } from '../claude/run';
9
9
  import type { OutputSink } from '../claude/types';
10
10
  import { type LoopEmitFn, type SendAgentEventFn } from '../cli/commands/loop';
11
+ import { type RunnerDependencies as CodexRunnerDependencies, run as codexRun } from '../codex/run';
11
12
  import type { SendEventFn } from './events';
12
13
  import { type LogBuffer } from './log-buffer';
13
14
  import type { RepositoryDependencies, RepositoryState } from './repository';
@@ -53,10 +54,20 @@ export interface LoopState {
53
54
  * Create an OutputSink that buffers stdout and logs complete lines.
54
55
  */
55
56
  export declare function createBufferStdoutSink(loopState: LoopState, logBuffer: LogBuffer): OutputSink;
57
+ /**
58
+ * Create a factory function that produces OutputSinks for agent runners.
59
+ * This factory captures loopState and logBuffer, returning a function
60
+ * that can be passed to RunnerDependencies.createStdoutSink.
61
+ */
62
+ export declare function createStdoutSinkFactory(loopState: LoopState, logBuffer: LogBuffer): () => OutputSink;
56
63
  /**
57
64
  * Create a run function that redirects Claude output to a log buffer.
58
65
  */
59
66
  export declare function createBufferRun(run: RepositoryDependencies['run'], bufferSinkDeps: RunnerDependencies): typeof claudeRun;
67
+ /**
68
+ * Create a run function that redirects Codex output to a log buffer.
69
+ */
70
+ export declare function createCodexBufferRun(run: typeof codexRun, codexBufferSinkDeps: CodexRunnerDependencies): typeof claudeRun;
60
71
  /** No-op postEvent for LoopDependencies. */
61
72
  export declare function noOpPostEvent(): Promise<void>;
62
73
  /**
@@ -10,6 +10,7 @@ import type { CommandDependencies, FileSystem } from '../cli/types';
10
10
  import type { DockerDependencies } from '../docker/docker-agent';
11
11
  import { type BucketEmitFn, type SendEventFn } from './events';
12
12
  import { type LogBuffer } from './log-buffer';
13
+ import type { ToolDefinition } from './server-messages';
13
14
  export { cloneRepository, getRepoPath, removeRepository, } from './repository-git';
14
15
  export { runRepositoryLoop } from './repository-loop';
15
16
  export interface Repository {
@@ -50,6 +51,8 @@ export interface RepositoryDependencies {
50
51
  getReposDir: () => string;
51
52
  /** Optional overrides for Docker dependency functions (for testing) */
52
53
  dockerDeps?: Partial<DockerDependencies>;
54
+ /** Function to get current tool definitions */
55
+ getTools?: () => ToolDefinition[];
53
56
  }
54
57
  /**
55
58
  * Start (or restart) the per-repository loop and keep loopPromise state accurate.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Typed server-to-client WebSocket messages for the bucket protocol.
3
+ */
4
+ import type { Repository } from './repository';
5
+ export interface RepositoryListMessage {
6
+ type: 'repository-list';
7
+ repositories: RepositoryListItem[];
8
+ }
9
+ export interface RepositoryListItem extends Repository {
10
+ hasTask: boolean;
11
+ }
12
+ export interface TaskAvailableMessage {
13
+ type: 'task-available';
14
+ repository: string;
15
+ }
16
+ export interface ToolParameter {
17
+ name: string;
18
+ type: 'string' | 'file' | 'number' | 'boolean';
19
+ required: boolean;
20
+ description: string;
21
+ }
22
+ export interface ToolDefinition {
23
+ name: string;
24
+ description: string;
25
+ endpoint: string;
26
+ method: 'GET' | 'POST';
27
+ parameters: ToolParameter[];
28
+ }
29
+ export interface ToolDefinitionsMessage {
30
+ type: 'tool-definitions';
31
+ tools: ToolDefinition[];
32
+ }
33
+ export type ServerMessage = RepositoryListMessage | TaskAvailableMessage | ToolDefinitionsMessage;
34
+ /**
35
+ * Parse and validate a server message from raw JSON data.
36
+ * Returns the typed message if valid, or null if invalid.
37
+ */
38
+ export declare function parseServerMessage(data: unknown): ServerMessage | null;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Formats tool definitions for injection into agent prompts.
3
+ */
4
+ import type { ToolDefinition } from './server-messages';
5
+ /**
6
+ * Format tool definitions into a markdown section for agent prompts.
7
+ * Returns an empty string if no tools are defined.
8
+ */
9
+ export declare function formatToolsSection(tools: ToolDefinition[]): string;
@@ -1,9 +1,13 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
2
  import { createInterface as nodeCreateInterface } from 'node:readline';
3
- import type { RawEvent, SpawnOptions } from './types';
3
+ import type { DockerSpawnConfig, RawEvent, SpawnOptions } from './types';
4
4
  export interface EventSourceDependencies {
5
5
  spawn: typeof nodeSpawn;
6
6
  createInterface: typeof nodeCreateInterface;
7
7
  }
8
8
  export declare const defaultDependencies: EventSourceDependencies;
9
+ /**
10
+ * Build docker run arguments for spawning claude in a container.
11
+ */
12
+ export declare function buildDockerRunArguments(docker: DockerSpawnConfig, claudeArguments: string[], env: Record<string, string>): string[];
9
13
  export declare function spawnClaudeCode(prompt: string, options?: SpawnOptions, dependencies?: EventSourceDependencies): AsyncGenerator<RawEvent>;
@@ -54,8 +54,10 @@ export interface DockerSpawnConfig {
54
54
  repoPath: string;
55
55
  /** Home directory for credential mounts */
56
56
  homeDir: string;
57
- /** Whether .gitconfig exists (for optional mount) */
58
- hasGitconfig: boolean;
57
+ /** Git credential proxy URL (e.g., http://host.docker.internal:3001) */
58
+ gitProxyUrl?: string;
59
+ /** Claude API proxy URL (e.g., http://host.docker.internal:3002) */
60
+ claudeApiProxyUrl?: string;
59
61
  }
60
62
  export interface SpawnOptions {
61
63
  cwd?: string;
@@ -107,6 +107,8 @@ interface IterationOptions {
107
107
  repositoryId?: string;
108
108
  /** Docker spawn config when running in Docker mode */
109
109
  docker?: DockerSpawnConfig;
110
+ /** Pre-formatted tools section to inject into the prompt */
111
+ toolsSection?: string;
110
112
  }
111
113
  export declare function runOneIteration(dependencies: CommandDependencies, loopDependencies: LoopDependencies, onLoopEvent: LoopEmitFn, onAgentEvent?: SendAgentEventFn, options?: IterationOptions): Promise<IterationResult>;
112
114
  export declare function parseMaxIterations(commandArguments: string[]): number;
@@ -41,4 +41,39 @@ export declare function buildDockerImage(config: DockerConfig, dependencies: Doc
41
41
  * Check if a Dockerfile exists at .dust/Dockerfile in the repository.
42
42
  */
43
43
  export declare function hasDockerfile(repoPath: string, dependencies: DockerDependencies): boolean;
44
+ type DockerPrepareEvent = {
45
+ type: 'loop.docker_detected';
46
+ imageTag: string;
47
+ } | {
48
+ type: 'loop.docker_building';
49
+ imageTag: string;
50
+ } | {
51
+ type: 'loop.docker_built';
52
+ imageTag: string;
53
+ } | {
54
+ type: 'loop.docker_error';
55
+ error: string;
56
+ };
57
+ interface DockerSpawnConfig {
58
+ imageTag: string;
59
+ repoPath: string;
60
+ homeDir: string;
61
+ }
62
+ type PrepareDockerConfigResult = {
63
+ config: DockerSpawnConfig;
64
+ } | {
65
+ error: string;
66
+ } | Record<string, never>;
67
+ /**
68
+ * Prepare Docker configuration for agent execution.
69
+ *
70
+ * Checks for a .dust/Dockerfile, verifies Docker availability, builds the image,
71
+ * and returns the spawn configuration. Emits events throughout the process.
72
+ *
73
+ * Returns:
74
+ * - `{ config: DockerSpawnConfig }` on success
75
+ * - `{ error: string }` on failure (Docker not available or build failed)
76
+ * - `{}` if no Dockerfile exists
77
+ */
78
+ export declare function prepareDockerConfig(repoPath: string, dependencies: DockerDependencies, onEvent: (event: DockerPrepareEvent) => void): Promise<PrepareDockerConfigResult>;
44
79
  export {};