@joshski/dust 0.1.79 → 0.1.80

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/dust.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // lib/cli/run.ts
6
- import { existsSync, statSync as statSync3 } from "node:fs";
6
+ import { existsSync as existsSync2, statSync as statSync3, writeSync } from "node:fs";
7
7
  import {
8
8
  chmod as chmod3,
9
9
  mkdir as mkdir3,
@@ -13,6 +13,18 @@ import {
13
13
  writeFile as writeFile3
14
14
  } from "node:fs/promises";
15
15
 
16
+ // lib/command-events.ts
17
+ function createEventEmitter(writeEvent) {
18
+ let sequence = 0;
19
+ return (event) => {
20
+ writeEvent({
21
+ sequence: sequence++,
22
+ timestamp: new Date().toISOString(),
23
+ event
24
+ });
25
+ };
26
+ }
27
+
16
28
  // lib/git/file-sorter.ts
17
29
  function createGitDirectoryFileSorter(gitRunner) {
18
30
  return async (dir, files) => {
@@ -306,7 +318,7 @@ async function loadSettings(cwd, fileSystem) {
306
318
  }
307
319
 
308
320
  // lib/version.ts
309
- var DUST_VERSION = "0.1.79";
321
+ var DUST_VERSION = "0.1.80";
310
322
 
311
323
  // lib/session.ts
312
324
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -1664,7 +1676,7 @@ async function audit(dependencies) {
1664
1676
  }
1665
1677
 
1666
1678
  // lib/cli/commands/bucket.ts
1667
- import { spawn as nodeSpawn4 } from "node:child_process";
1679
+ import { spawn as nodeSpawn5 } from "node:child_process";
1668
1680
  import { accessSync, statSync } from "node:fs";
1669
1681
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1670
1682
  import { homedir } from "node:os";
@@ -1710,9 +1722,9 @@ async function clearToken(fileSystem, homeDir) {
1710
1722
  throw error;
1711
1723
  }
1712
1724
  }
1713
- async function defaultExchangeCode(code) {
1725
+ async function defaultExchangeCode(code, fetchFn = fetch) {
1714
1726
  const host = getDustbucketHost();
1715
- const response = await fetch(`${host}/auth/cli/exchange`, {
1727
+ const response = await fetchFn(`${host}/auth/cli/exchange`, {
1716
1728
  method: "POST",
1717
1729
  headers: { "Content-Type": "application/json" },
1718
1730
  body: JSON.stringify({ code })
@@ -2021,6 +2033,31 @@ var defaultDependencies = {
2021
2033
  spawn: nodeSpawn2,
2022
2034
  createInterface: nodeCreateInterface
2023
2035
  };
2036
+ function buildDockerRunArguments(docker, claudeArguments, env) {
2037
+ const dockerArguments = [
2038
+ "run",
2039
+ "--rm",
2040
+ "-i",
2041
+ "-v",
2042
+ `${docker.repoPath}:/workspace`,
2043
+ "-w",
2044
+ "/workspace",
2045
+ "-v",
2046
+ `${docker.homeDir}/.claude:/root/.claude:ro`,
2047
+ "-v",
2048
+ `${docker.homeDir}/.ssh:/root/.ssh:ro`
2049
+ ];
2050
+ if (docker.hasGitconfig) {
2051
+ dockerArguments.push("-v", `${docker.homeDir}/.gitconfig:/root/.gitconfig:ro`);
2052
+ }
2053
+ for (const [key, value] of Object.entries(env)) {
2054
+ dockerArguments.push("-e", `${key}=${value}`);
2055
+ }
2056
+ dockerArguments.push(docker.imageTag);
2057
+ dockerArguments.push("claude");
2058
+ dockerArguments.push(...claudeArguments);
2059
+ return dockerArguments;
2060
+ }
2024
2061
  async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDependencies) {
2025
2062
  const {
2026
2063
  cwd,
@@ -2031,7 +2068,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
2031
2068
  sessionId,
2032
2069
  dangerouslySkipPermissions,
2033
2070
  env,
2034
- signal
2071
+ signal,
2072
+ docker
2035
2073
  } = options;
2036
2074
  const claudeArguments = [
2037
2075
  "-p",
@@ -2059,10 +2097,15 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
2059
2097
  if (dangerouslySkipPermissions) {
2060
2098
  claudeArguments.push("--dangerously-skip-permissions");
2061
2099
  }
2062
- const proc = dependencies.spawn("claude", claudeArguments, {
2100
+ const mergedEnv = { ...process.env, ...env };
2101
+ const proc = docker ? dependencies.spawn("docker", buildDockerRunArguments(docker, claudeArguments, env ?? {}), {
2063
2102
  cwd,
2064
2103
  stdio: ["ignore", "pipe", "pipe"],
2065
- env: { ...process.env, ...env }
2104
+ env: mergedEnv
2105
+ }) : dependencies.spawn("claude", claudeArguments, {
2106
+ cwd,
2107
+ stdio: ["ignore", "pipe", "pipe"],
2108
+ env: mergedEnv
2066
2109
  });
2067
2110
  if (!proc.stdout) {
2068
2111
  throw new Error("Failed to get stdout from claude process");
@@ -2442,7 +2485,11 @@ async function cloneRepository(repository, targetPath, spawn, context) {
2442
2485
  return new Promise((resolve) => {
2443
2486
  const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
2444
2487
  stdio: ["ignore", "pipe", "pipe"],
2445
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
2488
+ env: {
2489
+ ...process.env,
2490
+ GIT_TERMINAL_PROMPT: "0",
2491
+ GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
2492
+ }
2446
2493
  });
2447
2494
  let stderr = "";
2448
2495
  proc.stderr?.on("data", (data) => {
@@ -2522,7 +2569,66 @@ function formatAgentEvent(event) {
2522
2569
 
2523
2570
  // lib/cli/commands/loop.ts
2524
2571
  import { spawn as nodeSpawn3 } from "node:child_process";
2572
+ import { existsSync } from "node:fs";
2525
2573
  import os from "node:os";
2574
+ import path2 from "node:path";
2575
+
2576
+ // lib/docker/docker-agent.ts
2577
+ import path from "node:path";
2578
+ var log = createLogger("dust:docker:agent");
2579
+ async function isDockerAvailable(dependencies) {
2580
+ return new Promise((resolve) => {
2581
+ const proc = dependencies.spawn("docker", ["--version"], {
2582
+ stdio: ["ignore", "pipe", "pipe"]
2583
+ });
2584
+ proc.on("close", (code) => {
2585
+ resolve(code === 0);
2586
+ });
2587
+ proc.on("error", () => {
2588
+ resolve(false);
2589
+ });
2590
+ });
2591
+ }
2592
+ function generateImageTag(repoPath) {
2593
+ const repoName = path.basename(repoPath);
2594
+ const sanitized = repoName.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
2595
+ return `dust-agent-${sanitized}`;
2596
+ }
2597
+ async function buildDockerImage(config, dependencies) {
2598
+ const dockerfilePath = path.join(config.repoPath, ".dust", "Dockerfile");
2599
+ log(`building Docker image ${config.imageTag} from ${dockerfilePath}`);
2600
+ return new Promise((resolve) => {
2601
+ const proc = dependencies.spawn("docker", ["build", "-t", config.imageTag, "-f", dockerfilePath, config.repoPath], {
2602
+ stdio: ["ignore", "pipe", "pipe"]
2603
+ });
2604
+ let stderr = "";
2605
+ proc.stderr?.on("data", (data) => {
2606
+ stderr += data.toString();
2607
+ });
2608
+ proc.on("close", (code) => {
2609
+ if (code === 0) {
2610
+ log(`Docker image ${config.imageTag} built successfully`);
2611
+ resolve({ success: true });
2612
+ } else {
2613
+ log(`Docker build failed: ${stderr}`);
2614
+ resolve({
2615
+ success: false,
2616
+ error: `Docker build failed with exit code ${code}: ${stderr.trim()}`
2617
+ });
2618
+ }
2619
+ });
2620
+ proc.on("error", (error) => {
2621
+ resolve({
2622
+ success: false,
2623
+ error: `Docker build failed: ${error.message}`
2624
+ });
2625
+ });
2626
+ });
2627
+ }
2628
+ function hasDockerfile(repoPath, dependencies) {
2629
+ const dockerfilePath = path.join(repoPath, ".dust", "Dockerfile");
2630
+ return dependencies.existsSync(dockerfilePath);
2631
+ }
2526
2632
 
2527
2633
  // lib/artifacts/workflow-tasks.ts
2528
2634
  var IDEA_TRANSITION_PREFIXES = [
@@ -2530,10 +2636,79 @@ var IDEA_TRANSITION_PREFIXES = [
2530
2636
  "Decompose Idea: ",
2531
2637
  "Shelve Idea: "
2532
2638
  ];
2639
+ var CAPTURE_IDEA_PREFIX = "Add Idea: ";
2533
2640
  var EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
2534
2641
  function titleToFilename(title) {
2535
2642
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
2536
2643
  }
2644
+ var WORKFLOW_SECTION_HEADINGS = [
2645
+ { type: "refine", heading: "Refines Idea" },
2646
+ { type: "decompose-idea", heading: "Decomposes Idea" },
2647
+ { type: "shelve", heading: "Shelves Idea" }
2648
+ ];
2649
+ function extractIdeaSlugFromSection(content, sectionHeading) {
2650
+ const lines = content.split(`
2651
+ `);
2652
+ let inSection = false;
2653
+ for (const line of lines) {
2654
+ if (line.startsWith("## ")) {
2655
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
2656
+ continue;
2657
+ }
2658
+ if (!inSection)
2659
+ continue;
2660
+ if (line.startsWith("# "))
2661
+ break;
2662
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
2663
+ if (linkMatch) {
2664
+ const target = linkMatch[2];
2665
+ const slugMatch = target.match(/([^/]+)\.md$/);
2666
+ if (slugMatch) {
2667
+ return slugMatch[1];
2668
+ }
2669
+ }
2670
+ }
2671
+ return null;
2672
+ }
2673
+ async function findAllWorkflowTasks(fileSystem, dustPath) {
2674
+ const tasksPath = `${dustPath}/tasks`;
2675
+ const captureIdeaTasks = [];
2676
+ const workflowTasksByIdeaSlug = new Map;
2677
+ if (!fileSystem.exists(tasksPath)) {
2678
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
2679
+ }
2680
+ const files = await fileSystem.readdir(tasksPath);
2681
+ for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
2682
+ const content = await fileSystem.readFile(`${tasksPath}/${file}`);
2683
+ const titleMatch = content.match(/^#\s+(.+)$/m);
2684
+ if (!titleMatch)
2685
+ continue;
2686
+ const title = titleMatch[1].trim();
2687
+ const taskSlug = file.replace(/\.md$/, "");
2688
+ if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
2689
+ captureIdeaTasks.push({
2690
+ taskSlug,
2691
+ ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
2692
+ });
2693
+ } else if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
2694
+ captureIdeaTasks.push({
2695
+ taskSlug,
2696
+ ideaTitle: title.slice(EXPEDITE_IDEA_PREFIX.length)
2697
+ });
2698
+ }
2699
+ for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
2700
+ const linkedSlug = extractIdeaSlugFromSection(content, heading);
2701
+ if (linkedSlug) {
2702
+ workflowTasksByIdeaSlug.set(linkedSlug, {
2703
+ type,
2704
+ ideaSlug: linkedSlug,
2705
+ taskSlug
2706
+ });
2707
+ }
2708
+ }
2709
+ }
2710
+ return { captureIdeaTasks, workflowTasksByIdeaSlug };
2711
+ }
2537
2712
 
2538
2713
  // lib/cli/commands/focus.ts
2539
2714
  function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPath, installCommand) {
@@ -2669,9 +2844,25 @@ async function next(dependencies) {
2669
2844
  return { exitCode: 1 };
2670
2845
  }
2671
2846
  if (result.tasks.length === 0) {
2847
+ context.emitEvent?.({
2848
+ type: "tasks-listed",
2849
+ tasks: []
2850
+ });
2672
2851
  return { exitCode: 0 };
2673
2852
  }
2674
2853
  printTaskList(context, result.tasks);
2854
+ context.emitEvent?.({
2855
+ type: "tasks-listed",
2856
+ tasks: result.tasks.map((task) => {
2857
+ const parts = task.path.split("/");
2858
+ const filename = parts[parts.length - 1];
2859
+ return {
2860
+ path: task.path,
2861
+ title: task.title ?? filename.replace(".md", ""),
2862
+ blockedBy: []
2863
+ };
2864
+ })
2865
+ });
2675
2866
  return { exitCode: 0 };
2676
2867
  }
2677
2868
 
@@ -2708,6 +2899,14 @@ function formatLoopEvent(event) {
2708
2899
  return `Completed iteration ${event.iteration}/${event.maxIterations}`;
2709
2900
  case "loop.ended":
2710
2901
  return `Reached max iterations (${event.maxIterations}). Exiting.`;
2902
+ case "loop.docker_detected":
2903
+ return `Docker mode: found .dust/Dockerfile (image: ${event.imageTag})`;
2904
+ case "loop.docker_building":
2905
+ return `Building Docker image ${event.imageTag}...`;
2906
+ case "loop.docker_built":
2907
+ return `Docker image ${event.imageTag} ready`;
2908
+ case "loop.docker_error":
2909
+ return `Docker error: ${event.error}`;
2711
2910
  }
2712
2911
  }
2713
2912
  function createPostEvent(fetchFn) {
@@ -2747,7 +2946,7 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
2747
2946
  postEvent(eventsUrl, payload).catch(onError);
2748
2947
  };
2749
2948
  }
2750
- var log = createLogger("dust:cli:commands:loop");
2949
+ var log2 = createLogger("dust:cli:commands:loop");
2751
2950
  var SLEEP_INTERVAL_MS = 30000;
2752
2951
  var SLEEP_STEP_MS = 1000;
2753
2952
  var DEFAULT_MAX_ITERATIONS = 10;
@@ -2797,15 +2996,16 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
2797
2996
  onRawEvent,
2798
2997
  hooksInstalled = false,
2799
2998
  signal,
2800
- logger = log,
2801
- repositoryId
2999
+ logger = log2,
3000
+ repositoryId,
3001
+ docker
2802
3002
  } = options;
2803
3003
  const baseEnv = buildUnattendedEnv({ repositoryId });
2804
- log("syncing with remote");
3004
+ log2("syncing with remote");
2805
3005
  onLoopEvent({ type: "loop.syncing" });
2806
3006
  const pullResult = await gitPull(context.cwd, spawn);
2807
3007
  if (!pullResult.success) {
2808
- log(`git pull failed: ${pullResult.message}`);
3008
+ log2(`git pull failed: ${pullResult.message}`);
2809
3009
  onLoopEvent({
2810
3010
  type: "loop.sync_skipped",
2811
3011
  reason: pullResult.message
@@ -2836,7 +3036,8 @@ Make sure the repository is in a clean state and synced with remote before finis
2836
3036
  cwd: context.cwd,
2837
3037
  dangerouslySkipPermissions: true,
2838
3038
  env: baseEnv,
2839
- signal
3039
+ signal,
3040
+ docker
2840
3041
  },
2841
3042
  onRawEvent
2842
3043
  });
@@ -2855,12 +3056,12 @@ Make sure the repository is in a clean state and synced with remote before finis
2855
3056
  onLoopEvent({ type: "loop.checking_tasks" });
2856
3057
  const tasks = await findAvailableTasks(dependencies);
2857
3058
  if (tasks.length === 0) {
2858
- log("no tasks available");
3059
+ log2("no tasks available");
2859
3060
  onLoopEvent({ type: "loop.no_tasks" });
2860
3061
  return "no_tasks";
2861
3062
  }
2862
3063
  const task = tasks[0];
2863
- log(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
3064
+ log2(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
2864
3065
  onLoopEvent({ type: "loop.tasks_found" });
2865
3066
  const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
2866
3067
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
@@ -2888,11 +3089,12 @@ ${instructions}`;
2888
3089
  cwd: context.cwd,
2889
3090
  dangerouslySkipPermissions: true,
2890
3091
  env: baseEnv,
2891
- signal
3092
+ signal,
3093
+ docker
2892
3094
  },
2893
3095
  onRawEvent
2894
3096
  });
2895
- log(`${agentName} completed task: ${task.title ?? task.path}`);
3097
+ log2(`${agentName} completed task: ${task.title ?? task.path}`);
2896
3098
  onAgentEvent?.({ type: "agent-session-ended", success: true });
2897
3099
  return "ran_claude";
2898
3100
  } catch (error) {
@@ -2947,7 +3149,36 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2947
3149
  sendWireEvent(event);
2948
3150
  };
2949
3151
  const hooksInstalled = await manageGitHooks(dependencies);
2950
- log(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
3152
+ let dockerConfig;
3153
+ const dockerDeps = {
3154
+ spawn: loopDependencies.dockerDeps?.spawn ?? loopDependencies.spawn,
3155
+ homedir: loopDependencies.dockerDeps?.homedir ?? os.homedir,
3156
+ existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
3157
+ };
3158
+ if (hasDockerfile(context.cwd, dockerDeps)) {
3159
+ const imageTag = generateImageTag(context.cwd);
3160
+ onLoopEvent({ type: "loop.docker_detected", imageTag });
3161
+ if (!await isDockerAvailable(dockerDeps)) {
3162
+ context.stderr("Docker not available. Install Docker or remove .dust/Dockerfile to run without Docker.");
3163
+ return { exitCode: 1 };
3164
+ }
3165
+ onLoopEvent({ type: "loop.docker_building", imageTag });
3166
+ const buildResult = await buildDockerImage({ repoPath: context.cwd, imageTag }, dockerDeps);
3167
+ if (!buildResult.success) {
3168
+ onLoopEvent({ type: "loop.docker_error", error: buildResult.error });
3169
+ context.stderr(buildResult.error);
3170
+ return { exitCode: 1 };
3171
+ }
3172
+ onLoopEvent({ type: "loop.docker_built", imageTag });
3173
+ const homeDir = os.homedir();
3174
+ dockerConfig = {
3175
+ imageTag,
3176
+ repoPath: context.cwd,
3177
+ homeDir,
3178
+ hasGitconfig: existsSync(path2.join(homeDir, ".gitconfig"))
3179
+ };
3180
+ }
3181
+ log2(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
2951
3182
  onLoopEvent({ type: "loop.warning" });
2952
3183
  onLoopEvent({
2953
3184
  type: "loop.started",
@@ -2957,7 +3188,10 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2957
3188
  context.stdout(" Press Ctrl+C to stop");
2958
3189
  context.stdout("");
2959
3190
  let completedIterations = 0;
2960
- const iterationOptions = { hooksInstalled };
3191
+ const iterationOptions = {
3192
+ hooksInstalled,
3193
+ docker: dockerConfig
3194
+ };
2961
3195
  if (eventsUrl) {
2962
3196
  iterationOptions.onRawEvent = createHeartbeatThrottler(onAgentEvent);
2963
3197
  }
@@ -2965,12 +3199,12 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2965
3199
  agentSessionId = crypto.randomUUID();
2966
3200
  const result = await runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, iterationOptions);
2967
3201
  if (result === "no_tasks") {
2968
- log("sleeping, no tasks");
3202
+ log2("sleeping, no tasks");
2969
3203
  const writeInline = context.stdoutInline ?? context.stdout;
2970
3204
  await sleepWithProgress(loopDependencies.sleep, SLEEP_INTERVAL_MS, writeInline, context.stdout);
2971
3205
  } else {
2972
3206
  completedIterations++;
2973
- log(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
3207
+ log2(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
2974
3208
  onLoopEvent({
2975
3209
  type: "loop.iteration_complete",
2976
3210
  iteration: completedIterations,
@@ -2978,13 +3212,135 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
2978
3212
  });
2979
3213
  }
2980
3214
  }
2981
- log(`loop ended after ${completedIterations} iterations`);
3215
+ log2(`loop ended after ${completedIterations} iterations`);
2982
3216
  onLoopEvent({ type: "loop.ended", maxIterations });
2983
3217
  return { exitCode: 0 };
2984
3218
  }
2985
3219
 
3220
+ // lib/codex/spawn-codex.ts
3221
+ import { spawn as nodeSpawn4 } from "node:child_process";
3222
+ import { createInterface as nodeCreateInterface2 } from "node:readline";
3223
+ var debug2 = createLogger("dust.codex.spawn-codex");
3224
+ var defaultDependencies2 = {
3225
+ spawn: nodeSpawn4,
3226
+ createInterface: nodeCreateInterface2
3227
+ };
3228
+ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
3229
+ const { cwd, env, signal } = options;
3230
+ const codexArguments = ["exec", prompt, "--json", "--yolo"];
3231
+ if (cwd) {
3232
+ codexArguments.push("--cd", cwd);
3233
+ }
3234
+ const proc = dependencies.spawn("codex", codexArguments, {
3235
+ stdio: ["ignore", "pipe", "pipe"],
3236
+ env: { ...process.env, ...env }
3237
+ });
3238
+ if (!proc.stdout) {
3239
+ throw new Error("Failed to get stdout from codex process");
3240
+ }
3241
+ let stderrOutput = "";
3242
+ proc.stderr?.on("data", (data) => {
3243
+ stderrOutput += data.toString();
3244
+ });
3245
+ const closePromise = new Promise((resolve, reject) => {
3246
+ proc.on("close", (code) => {
3247
+ if (code === 0 || code === null)
3248
+ resolve();
3249
+ else {
3250
+ const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
3251
+ reject(new Error(errMsg));
3252
+ }
3253
+ });
3254
+ proc.on("error", reject);
3255
+ });
3256
+ const abortHandler = () => {
3257
+ if (!proc.killed) {
3258
+ proc.kill();
3259
+ }
3260
+ };
3261
+ if (signal?.aborted) {
3262
+ abortHandler();
3263
+ } else if (signal) {
3264
+ signal.addEventListener("abort", abortHandler, { once: true });
3265
+ }
3266
+ const rl = dependencies.createInterface({ input: proc.stdout });
3267
+ try {
3268
+ for await (const line of rl) {
3269
+ if (!line.trim())
3270
+ continue;
3271
+ try {
3272
+ yield JSON.parse(line);
3273
+ } catch {
3274
+ debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
3275
+ }
3276
+ }
3277
+ await closePromise;
3278
+ } finally {
3279
+ signal?.removeEventListener("abort", abortHandler);
3280
+ rl.close?.();
3281
+ }
3282
+ }
3283
+
3284
+ // lib/codex/event-parser.ts
3285
+ function* parseCodexRawEvent(raw) {
3286
+ if (raw.type !== "item.completed")
3287
+ return;
3288
+ const item = raw.item;
3289
+ if (!item)
3290
+ return;
3291
+ if (item.type === "agent_message" && typeof item.text === "string") {
3292
+ yield { type: "text_delta", text: `${item.text}
3293
+ ` };
3294
+ } else if (item.type === "command_execution" && typeof item.command === "string") {
3295
+ yield {
3296
+ type: "tool_use",
3297
+ id: typeof item.id === "string" ? item.id : "",
3298
+ name: "command_execution",
3299
+ input: { command: item.command }
3300
+ };
3301
+ if (typeof item.aggregated_output === "string") {
3302
+ yield {
3303
+ type: "tool_result",
3304
+ toolUseId: typeof item.id === "string" ? item.id : "",
3305
+ content: item.aggregated_output || `(exit code: ${item.exit_code ?? "unknown"})`
3306
+ };
3307
+ }
3308
+ }
3309
+ }
3310
+
3311
+ // lib/codex/streamer.ts
3312
+ async function streamCodexEvents(events, sink, onRawEvent) {
3313
+ let hadTextOutput = false;
3314
+ for await (const raw of events) {
3315
+ onRawEvent?.(raw);
3316
+ for (const event of parseCodexRawEvent(raw)) {
3317
+ processEvent(event, sink, { hadTextOutput });
3318
+ if (event.type === "text_delta") {
3319
+ hadTextOutput = true;
3320
+ } else if (event.type === "tool_use") {
3321
+ hadTextOutput = false;
3322
+ }
3323
+ }
3324
+ }
3325
+ }
3326
+
3327
+ // lib/codex/run.ts
3328
+ var defaultRunnerDependencies2 = {
3329
+ spawnCodex,
3330
+ createStdoutSink,
3331
+ streamCodexEvents
3332
+ };
3333
+ async function run2(prompt, options = {}, dependencies = defaultRunnerDependencies2) {
3334
+ const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
3335
+ const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
3336
+ const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
3337
+ const events = dependencies.spawnCodex(prompt, spawnOptions);
3338
+ const sink = dependencies.createStdoutSink();
3339
+ await dependencies.streamCodexEvents(events, sink, onRawEvent);
3340
+ }
3341
+
2986
3342
  // lib/bucket/repository-loop.ts
2987
- var log2 = createLogger("dust:bucket:repository-loop");
3343
+ var log3 = createLogger("dust:bucket:repository-loop");
2988
3344
  var FALLBACK_TIMEOUT_MS = 300000;
2989
3345
  function createLogCallbacks(logBuffer) {
2990
3346
  return {
@@ -3030,59 +3386,40 @@ function createWakeUpHandler(repoState, resolve) {
3030
3386
  }
3031
3387
  function createNoOpGlobScanner() {
3032
3388
  return {
3033
- scan: async function* () {}
3389
+ scan: async function* noOpScan() {}
3034
3390
  };
3035
3391
  }
3036
- async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3037
- const { spawn, run: run2, fileSystem, sleep } = repoDeps;
3038
- const repoName = repoState.repository.name;
3039
- const settings = await loadSettings(repoState.path, fileSystem);
3040
- const logCallbacks = createLogCallbacks(repoState.logBuffer);
3041
- const commandDeps = {
3042
- arguments: [],
3043
- context: {
3044
- cwd: repoState.path,
3045
- stdout: (msg) => logCallbacks.stdout(msg),
3046
- stderr: (msg) => logCallbacks.stderr(msg)
3047
- },
3048
- fileSystem,
3049
- globScanner: createNoOpGlobScanner(),
3050
- settings
3051
- };
3052
- let partialLine = "";
3053
- const bufferSinkDeps = {
3054
- ...defaultRunnerDependencies,
3055
- createStdoutSink: () => ({
3056
- write: (text) => {
3057
- partialLine += text;
3058
- const lines = partialLine.split(`
3392
+ function createBufferStdoutSink(loopState, logBuffer) {
3393
+ return {
3394
+ write(text) {
3395
+ loopState.partialLine += text;
3396
+ const lines = loopState.partialLine.split(`
3059
3397
  `);
3060
- for (let i = 0;i < lines.length - 1; i++) {
3061
- appendLogLine(repoState.logBuffer, createLogLine(lines[i], "stdout"));
3062
- }
3063
- partialLine = lines[lines.length - 1];
3064
- },
3065
- line: (text) => {
3066
- partialLine = flushAndLogMultiLine(partialLine, text, repoState.logBuffer);
3398
+ for (let i = 0;i < lines.length - 1; i++) {
3399
+ appendLogLine(logBuffer, createLogLine(lines[i], "stdout"));
3067
3400
  }
3068
- })
3069
- };
3070
- const bufferRun = (prompt, options) => run2(prompt, options, bufferSinkDeps);
3071
- const loopDeps = {
3072
- spawn,
3073
- run: bufferRun,
3074
- sleep,
3075
- postEvent: async () => {}
3401
+ loopState.partialLine = lines[lines.length - 1];
3402
+ },
3403
+ line(text) {
3404
+ loopState.partialLine = flushAndLogMultiLine(loopState.partialLine, text, logBuffer);
3405
+ }
3076
3406
  };
3077
- let agentSessionId;
3078
- let sequence = 0;
3079
- const onLoopEvent = (event) => {
3407
+ }
3408
+ function createBufferRun(run3, bufferSinkDeps) {
3409
+ return (prompt, options) => run3(prompt, options, bufferSinkDeps);
3410
+ }
3411
+ async function noOpPostEvent() {}
3412
+ function createLoopEventHandler(logBuffer) {
3413
+ return function onLoopEvent(event) {
3080
3414
  const formatted = formatLoopEvent(event);
3081
3415
  if (formatted !== null) {
3082
- appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
3416
+ appendLogLine(logBuffer, createLogLine(formatted, "stdout"));
3083
3417
  }
3084
3418
  };
3085
- const onAgentEvent = (event) => {
3419
+ }
3420
+ function createAgentEventHandler(parameters) {
3421
+ const { repoState, sendEvent, sessionId, repoName, loopState } = parameters;
3422
+ return function onAgentEvent(event) {
3086
3423
  if (event.type === "agent-session-started") {
3087
3424
  repoState.agentStatus = "busy";
3088
3425
  } else if (event.type === "agent-session-ended") {
@@ -3093,26 +3430,88 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3093
3430
  appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
3094
3431
  }
3095
3432
  if (sendEvent && sessionId) {
3096
- sequence++;
3433
+ loopState.sequence++;
3097
3434
  sendEvent(buildEventMessage({
3098
- sequence,
3435
+ sequence: loopState.sequence,
3099
3436
  sessionId,
3100
3437
  repository: repoName,
3101
3438
  repoId: repoState.repository.id,
3102
3439
  event,
3103
- agentSessionId
3440
+ agentSessionId: loopState.agentSessionId
3104
3441
  }));
3105
3442
  }
3106
3443
  };
3444
+ }
3445
+ function createCancelHandler(abortController) {
3446
+ return abortController.abort.bind(abortController);
3447
+ }
3448
+ function setupFallbackTimeout(repoState, sleep, resolve, wakeUpForThisWait) {
3449
+ sleep(FALLBACK_TIMEOUT_MS).then(function onFallbackTimeout() {
3450
+ if (repoState.wakeUp === wakeUpForThisWait) {
3451
+ repoState.wakeUp = undefined;
3452
+ resolve();
3453
+ }
3454
+ });
3455
+ }
3456
+ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3457
+ const { spawn, run: run3, fileSystem, sleep } = repoDeps;
3458
+ const repoName = repoState.repository.name;
3459
+ const settings = await loadSettings(repoState.path, fileSystem);
3460
+ const logCallbacks = createLogCallbacks(repoState.logBuffer);
3461
+ const commandDeps = {
3462
+ arguments: [],
3463
+ context: {
3464
+ cwd: repoState.path,
3465
+ stdout: logCallbacks.stdout,
3466
+ stderr: logCallbacks.stderr
3467
+ },
3468
+ fileSystem,
3469
+ globScanner: createNoOpGlobScanner(),
3470
+ settings
3471
+ };
3472
+ const loopState = {
3473
+ partialLine: "",
3474
+ sequence: 0,
3475
+ agentSessionId: undefined
3476
+ };
3477
+ const isCodex = repoState.repository.agentProvider === "codex";
3478
+ const agentType = isCodex ? "codex" : "claude";
3479
+ const createStdoutSink2 = () => createBufferStdoutSink(loopState, repoState.logBuffer);
3480
+ let bufferRun;
3481
+ if (isCodex) {
3482
+ const codexBufferSinkDeps = {
3483
+ ...defaultRunnerDependencies2,
3484
+ createStdoutSink: createStdoutSink2
3485
+ };
3486
+ bufferRun = (prompt, options) => run2(prompt, options, codexBufferSinkDeps);
3487
+ } else {
3488
+ const bufferSinkDeps = {
3489
+ ...defaultRunnerDependencies,
3490
+ createStdoutSink: createStdoutSink2
3491
+ };
3492
+ bufferRun = createBufferRun(run3, bufferSinkDeps);
3493
+ }
3494
+ const loopDeps = {
3495
+ spawn,
3496
+ run: bufferRun,
3497
+ sleep,
3498
+ postEvent: noOpPostEvent,
3499
+ agentType
3500
+ };
3501
+ const onLoopEvent = createLoopEventHandler(repoState.logBuffer);
3502
+ const onAgentEvent = createAgentEventHandler({
3503
+ repoState,
3504
+ sendEvent,
3505
+ sessionId,
3506
+ repoName,
3507
+ loopState
3508
+ });
3107
3509
  const hooksInstalled = await manageGitHooks(commandDeps);
3108
- const logLine = (msg) => appendLogLine(repoState.logBuffer, createLogLine(msg, "stdout"));
3109
- log2(`loop started for ${repoName} at ${repoState.path}`);
3510
+ log3(`loop started for ${repoName} at ${repoState.path}`);
3110
3511
  while (!repoState.stopRequested) {
3111
- agentSessionId = crypto.randomUUID();
3512
+ loopState.agentSessionId = crypto.randomUUID();
3112
3513
  const abortController = new AbortController;
3113
- const cancelCurrentIteration = () => {
3114
- abortController.abort();
3115
- };
3514
+ const cancelCurrentIteration = createCancelHandler(abortController);
3116
3515
  repoState.cancelCurrentIteration = cancelCurrentIteration;
3117
3516
  let result;
3118
3517
  try {
@@ -3124,7 +3523,7 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3124
3523
  });
3125
3524
  } catch (error) {
3126
3525
  const msg = error instanceof Error ? error.message : String(error);
3127
- log2(`iteration error for ${repoName}: ${msg}`);
3526
+ log3(`iteration error for ${repoName}: ${msg}`);
3128
3527
  appendLogLine(repoState.logBuffer, createLogLine(`Loop error: ${msg}`, "stderr"));
3129
3528
  await sleep(1e4);
3130
3529
  continue;
@@ -3136,39 +3535,34 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3136
3535
  if (result === "no_tasks") {
3137
3536
  if (repoState.taskAvailablePending) {
3138
3537
  repoState.taskAvailablePending = false;
3139
- log2(`${repoName}: task signal received during iteration, rechecking`);
3140
- logLine("Task signal received during iteration, rechecking...");
3538
+ log3(`${repoName}: task signal received during iteration, rechecking`);
3539
+ appendLogLine(repoState.logBuffer, createLogLine("Task signal received during iteration, rechecking...", "stdout"));
3141
3540
  continue;
3142
3541
  }
3143
- log2(`${repoName}: no tasks available, waiting`);
3144
- logLine("Waiting for tasks...");
3145
- await new Promise((resolve) => {
3542
+ log3(`${repoName}: no tasks available, waiting`);
3543
+ appendLogLine(repoState.logBuffer, createLogLine("Waiting for tasks...", "stdout"));
3544
+ await new Promise(function waitForTasks(resolve) {
3146
3545
  const wakeUpForThisWait = createWakeUpHandler(repoState, resolve);
3147
3546
  repoState.wakeUp = wakeUpForThisWait;
3148
- sleep(FALLBACK_TIMEOUT_MS).then(() => {
3149
- if (repoState.wakeUp === wakeUpForThisWait) {
3150
- repoState.wakeUp = undefined;
3151
- resolve();
3152
- }
3153
- });
3547
+ setupFallbackTimeout(repoState, sleep, resolve, wakeUpForThisWait);
3154
3548
  });
3155
3549
  }
3156
3550
  }
3157
- log2(`loop stopped for ${repoName}`);
3551
+ log3(`loop stopped for ${repoName}`);
3158
3552
  appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
3159
3553
  }
3160
3554
 
3161
3555
  // lib/bucket/repository.ts
3162
- var log3 = createLogger("dust:bucket:repository");
3556
+ var log4 = createLogger("dust:bucket:repository");
3163
3557
  function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
3164
- log3(`starting loop for ${repoState.repository.name}`);
3558
+ log4(`starting loop for ${repoState.repository.name}`);
3165
3559
  repoState.stopRequested = false;
3166
3560
  repoState.loopPromise = runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId).catch((error) => {
3167
3561
  const message = error instanceof Error ? error.message : String(error);
3168
- log3(`loop crashed for ${repoState.repository.name}: ${message}`);
3562
+ log4(`loop crashed for ${repoState.repository.name}: ${message}`);
3169
3563
  appendLogLine(repoState.logBuffer, createLogLine(`Repository loop crashed: ${message}`, "stderr"));
3170
3564
  }).finally(() => {
3171
- log3(`loop finished for ${repoState.repository.name}`);
3565
+ log4(`loop finished for ${repoState.repository.name}`);
3172
3566
  repoState.loopPromise = null;
3173
3567
  repoState.agentStatus = "idle";
3174
3568
  repoState.wakeUp = undefined;
@@ -3191,10 +3585,10 @@ function parseRepository(data) {
3191
3585
  }
3192
3586
  async function addRepository(repository, manager, repoDeps, context) {
3193
3587
  if (manager.repositories.has(repository.name)) {
3194
- log3(`repository ${repository.name} already exists, skipping add`);
3588
+ log4(`repository ${repository.name} already exists, skipping add`);
3195
3589
  return;
3196
3590
  }
3197
- log3(`adding repository ${repository.name}`);
3591
+ log4(`adding repository ${repository.name}`);
3198
3592
  const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
3199
3593
  await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
3200
3594
  if (repoDeps.fileSystem.exists(repoPath)) {
@@ -3234,7 +3628,7 @@ async function removeRepositoryFromManager(repoName, manager, repoDeps, context)
3234
3628
  if (!repoState) {
3235
3629
  return;
3236
3630
  }
3237
- log3(`removing repository ${repoName}`);
3631
+ log4(`removing repository ${repoName}`);
3238
3632
  repoState.stopRequested = true;
3239
3633
  repoState.cancelCurrentIteration?.();
3240
3634
  repoState.wakeUp?.();
@@ -3292,13 +3686,17 @@ function parseServerMessage(data) {
3292
3686
  if (typeof repo.id !== "number" || typeof repo.url !== "string" || typeof repo.hasTask !== "boolean") {
3293
3687
  return null;
3294
3688
  }
3295
- repositories.push({
3689
+ const item = {
3296
3690
  id: repo.id,
3297
3691
  name: repo.name,
3298
3692
  gitUrl: repo.gitUrl,
3299
3693
  url: repo.url,
3300
3694
  hasTask: repo.hasTask
3301
- });
3695
+ };
3696
+ if (typeof repo.agentProvider === "string") {
3697
+ item.agentProvider = repo.agentProvider;
3698
+ }
3699
+ repositories.push(item);
3302
3700
  }
3303
3701
  return { type: "repository-list", repositories };
3304
3702
  }
@@ -3739,7 +4137,7 @@ function handleKeyInput(state, key, options) {
3739
4137
  }
3740
4138
 
3741
4139
  // lib/cli/commands/bucket.ts
3742
- var log4 = createLogger("dust:cli:commands:bucket");
4140
+ var log5 = createLogger("dust:cli:commands:bucket");
3743
4141
  var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
3744
4142
  var INITIAL_RECONNECT_DELAY_MS = 1000;
3745
4143
  var MAX_RECONNECT_DELAY_MS = 30000;
@@ -3799,27 +4197,27 @@ function defaultWriteStdout(data) {
3799
4197
  }
3800
4198
  function createAuthFileSystem(dependencies) {
3801
4199
  return {
3802
- exists: (path) => {
4200
+ exists: (path3) => {
3803
4201
  try {
3804
- dependencies.accessSync(path);
4202
+ dependencies.accessSync(path3);
3805
4203
  return true;
3806
4204
  } catch {
3807
4205
  return false;
3808
4206
  }
3809
4207
  },
3810
- isDirectory: (path) => {
4208
+ isDirectory: (path3) => {
3811
4209
  try {
3812
- return dependencies.statSync(path).isDirectory();
4210
+ return dependencies.statSync(path3).isDirectory();
3813
4211
  } catch {
3814
4212
  return false;
3815
4213
  }
3816
4214
  },
3817
- getFileCreationTime: (path) => dependencies.statSync(path).birthtimeMs,
3818
- readFile: (path) => dependencies.readFile(path, "utf8"),
3819
- writeFile: (path, content) => dependencies.writeFile(path, content, "utf8"),
3820
- mkdir: (path, options) => dependencies.mkdir(path, options).then(() => {}),
3821
- readdir: (path) => dependencies.readdir(path),
3822
- chmod: (path, mode) => dependencies.chmod(path, mode),
4215
+ getFileCreationTime: (path3) => dependencies.statSync(path3).birthtimeMs,
4216
+ readFile: (path3) => dependencies.readFile(path3, "utf8"),
4217
+ writeFile: (path3, content) => dependencies.writeFile(path3, content, "utf8"),
4218
+ mkdir: (path3, options) => dependencies.mkdir(path3, options).then(() => {}),
4219
+ readdir: (path3) => dependencies.readdir(path3),
4220
+ chmod: (path3, mode) => dependencies.chmod(path3, mode),
3823
4221
  rename: (oldPath, newPath) => dependencies.rename(oldPath, newPath)
3824
4222
  };
3825
4223
  }
@@ -3835,7 +4233,7 @@ function createDefaultBucketDependencies() {
3835
4233
  rename: (oldPath, newPath) => import("node:fs/promises").then((mod) => mod.rename(oldPath, newPath))
3836
4234
  });
3837
4235
  return {
3838
- spawn: nodeSpawn4,
4236
+ spawn: nodeSpawn5,
3839
4237
  createWebSocket: defaultCreateWebSocket,
3840
4238
  setupKeypress: defaultSetupKeypress,
3841
4239
  setupSignals: defaultSetupSignals,
@@ -3887,20 +4285,20 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
3887
4285
  }
3888
4286
  function ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI) {
3889
4287
  if (repoState.loopPromise || repoState.wakeUp || repoState.stopRequested) {
3890
- log4(`loop already running/waiting for ${repoState.repository.name}`);
4288
+ log5(`loop already running/waiting for ${repoState.repository.name}`);
3891
4289
  return;
3892
4290
  }
3893
4291
  logMessage(state, context, useTUI, `Repository loop not running for ${repoState.repository.name}; restarting`);
3894
4292
  startRepositoryLoop(repoState, repoDeps, state.sendEvent, state.sessionId);
3895
4293
  }
3896
4294
  function signalTaskAvailable(repoState, state, repoDeps, context, useTUI) {
3897
- log4(`task-available signal for ${repoState.repository.name}`);
4295
+ log5(`task-available signal for ${repoState.repository.name}`);
3898
4296
  ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI);
3899
4297
  if (repoState.wakeUp) {
3900
- log4(`waking loop for ${repoState.repository.name}`);
4298
+ log5(`waking loop for ${repoState.repository.name}`);
3901
4299
  repoState.wakeUp();
3902
4300
  } else {
3903
- log4(`marking task pending for ${repoState.repository.name} (loop busy)`);
4301
+ log5(`marking task pending for ${repoState.repository.name} (loop busy)`);
3904
4302
  repoState.taskAvailablePending = true;
3905
4303
  }
3906
4304
  }
@@ -4034,7 +4432,7 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
4034
4432
  logMessage(state, context, useTUI, `Invalid WebSocket message format: ${event.data}`, "stderr");
4035
4433
  return;
4036
4434
  }
4037
- log4(`ws message: ${message.type}`);
4435
+ log5(`ws message: ${message.type}`);
4038
4436
  if (message.type === "repository-list") {
4039
4437
  const repos = message.repositories;
4040
4438
  logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories):`);
@@ -4084,7 +4482,7 @@ async function shutdown(state, bucketDeps, context) {
4084
4482
  if (state.shuttingDown)
4085
4483
  return;
4086
4484
  state.shuttingDown = true;
4087
- log4("shutdown initiated");
4485
+ log5("shutdown initiated");
4088
4486
  context.stdout("Shutting down...");
4089
4487
  if (state.reconnectTimer) {
4090
4488
  clearTimeout(state.reconnectTimer);
@@ -4287,16 +4685,16 @@ function createDefaultUploadDependencies() {
4287
4685
  getHomeDir: () => homedir2(),
4288
4686
  fileSystem: authFileSystem
4289
4687
  },
4290
- readFileBytes: async (path) => {
4291
- const buffer = await Bun.file(path).arrayBuffer();
4688
+ readFileBytes: async (path3) => {
4689
+ const buffer = await Bun.file(path3).arrayBuffer();
4292
4690
  return new Uint8Array(buffer);
4293
4691
  },
4294
- getFileSize: async (path) => {
4295
- const file = Bun.file(path);
4692
+ getFileSize: async (path3) => {
4693
+ const file = Bun.file(path3);
4296
4694
  return file.size;
4297
4695
  },
4298
- fileExists: async (path) => {
4299
- const file = Bun.file(path);
4696
+ fileExists: async (path3) => {
4697
+ const file = Bun.file(path3);
4300
4698
  return file.exists();
4301
4699
  },
4302
4700
  uploadFile: async (url, token, fileBytes, contentType, fileName) => {
@@ -5090,12 +5488,12 @@ function validateNoCycles(allPrincipleRelationships) {
5090
5488
  }
5091
5489
  for (const rel of allPrincipleRelationships) {
5092
5490
  const visited = new Set;
5093
- const path = [];
5491
+ const path3 = [];
5094
5492
  let current = rel.filePath;
5095
5493
  while (current) {
5096
5494
  if (visited.has(current)) {
5097
- const cycleStart = path.indexOf(current);
5098
- const cyclePath = path.slice(cycleStart).concat(current);
5495
+ const cycleStart = path3.indexOf(current);
5496
+ const cyclePath = path3.slice(cycleStart).concat(current);
5099
5497
  violations.push({
5100
5498
  file: rel.filePath,
5101
5499
  message: `Cycle detected in principle hierarchy: ${cyclePath.join(" -> ")}`
@@ -5103,7 +5501,7 @@ function validateNoCycles(allPrincipleRelationships) {
5103
5501
  break;
5104
5502
  }
5105
5503
  visited.add(current);
5106
- path.push(current);
5504
+ path3.push(current);
5107
5505
  const currentRel = relationshipMap.get(current);
5108
5506
  if (currentRel && currentRel.parentPrinciples.length > 0) {
5109
5507
  current = currentRel.parentPrinciples[0];
@@ -5305,7 +5703,7 @@ async function lintMarkdown(dependencies) {
5305
5703
  }
5306
5704
 
5307
5705
  // lib/cli/commands/check.ts
5308
- var log5 = createLogger("dust:cli:commands:check");
5706
+ var log6 = createLogger("dust:cli:commands:check");
5309
5707
  var DEFAULT_CHECK_TIMEOUT_MS = 13000;
5310
5708
  var MAX_OUTPUT_LINES = 500;
5311
5709
  var KEEP_LINES = 250;
@@ -5325,14 +5723,27 @@ function truncateOutput(output) {
5325
5723
  ].join(`
5326
5724
  `);
5327
5725
  }
5328
- async function runSingleCheck(check, cwd, runner) {
5726
+ async function runSingleCheck(check, cwd, runner, emitEvent) {
5329
5727
  const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
5330
- log5(`running check ${check.name}: ${check.command}`);
5728
+ log6(`running check ${check.name}: ${check.command}`);
5729
+ emitEvent?.({ type: "check-started", name: check.name });
5331
5730
  const startTime = Date.now();
5332
5731
  const result = await runner.run(check.command, cwd, timeoutMs);
5333
5732
  const durationMs = Date.now() - startTime;
5334
5733
  const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
5335
- log5(`check ${check.name} ${status} (${durationMs}ms)`);
5734
+ log6(`check ${check.name} ${status} (${durationMs}ms)`);
5735
+ if (result.exitCode === 0) {
5736
+ emitEvent?.({ type: "check-passed", name: check.name, durationMs });
5737
+ } else {
5738
+ const failedEvent = {
5739
+ type: "check-failed",
5740
+ name: check.name,
5741
+ durationMs
5742
+ };
5743
+ if (result.output)
5744
+ failedEvent.output = result.output;
5745
+ emitEvent?.(failedEvent);
5746
+ }
5336
5747
  return {
5337
5748
  name: check.name,
5338
5749
  command: check.command,
@@ -5344,25 +5755,26 @@ async function runSingleCheck(check, cwd, runner) {
5344
5755
  timeoutSeconds: timeoutMs / 1000
5345
5756
  };
5346
5757
  }
5347
- async function runConfiguredChecks(checks, cwd, runner) {
5348
- const promises = checks.map((check) => runSingleCheck(check, cwd, runner));
5758
+ async function runConfiguredChecks(checks, cwd, runner, emitEvent) {
5759
+ const promises = checks.map((check) => runSingleCheck(check, cwd, runner, emitEvent));
5349
5760
  return Promise.all(promises);
5350
5761
  }
5351
- async function runConfiguredChecksSerially(checks, cwd, runner) {
5762
+ async function runConfiguredChecksSerially(checks, cwd, runner, emitEvent) {
5352
5763
  const results = [];
5353
5764
  for (const check of checks) {
5354
- results.push(await runSingleCheck(check, cwd, runner));
5765
+ results.push(await runSingleCheck(check, cwd, runner, emitEvent));
5355
5766
  }
5356
5767
  return results;
5357
5768
  }
5358
- async function runValidationCheck(dependencies) {
5769
+ async function runValidationCheck(dependencies, emitEvent) {
5359
5770
  const outputLines = [];
5360
5771
  const bufferedContext = {
5361
5772
  cwd: dependencies.context.cwd,
5362
5773
  stdout: (msg) => outputLines.push(msg),
5363
5774
  stderr: (msg) => outputLines.push(msg)
5364
5775
  };
5365
- log5("running built-in check: dust lint");
5776
+ log6("running built-in check: dust lint");
5777
+ emitEvent?.({ type: "check-started", name: "lint" });
5366
5778
  const startTime = Date.now();
5367
5779
  const result = await lintMarkdown({
5368
5780
  ...dependencies,
@@ -5371,13 +5783,26 @@ async function runValidationCheck(dependencies) {
5371
5783
  });
5372
5784
  const durationMs = Date.now() - startTime;
5373
5785
  const lintStatus = result.exitCode === 0 ? "passed" : "failed";
5374
- log5(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5786
+ log6(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
5787
+ const output = outputLines.join(`
5788
+ `);
5789
+ if (result.exitCode === 0) {
5790
+ emitEvent?.({ type: "check-passed", name: "lint", durationMs });
5791
+ } else {
5792
+ const failedEvent = {
5793
+ type: "check-failed",
5794
+ name: "lint",
5795
+ durationMs
5796
+ };
5797
+ if (output)
5798
+ failedEvent.output = output;
5799
+ emitEvent?.(failedEvent);
5800
+ }
5375
5801
  return {
5376
5802
  name: "lint",
5377
5803
  command: "dust lint",
5378
5804
  exitCode: result.exitCode,
5379
- output: outputLines.join(`
5380
- `),
5805
+ output,
5381
5806
  isBuiltIn: true,
5382
5807
  durationMs,
5383
5808
  timedOut: false
@@ -5446,18 +5871,18 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
5446
5871
  if (serial) {
5447
5872
  const results2 = [];
5448
5873
  if (hasDustDir) {
5449
- results2.push(await runValidationCheck(dependencies));
5874
+ results2.push(await runValidationCheck(dependencies, context.emitEvent));
5450
5875
  }
5451
- const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner);
5876
+ const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner, context.emitEvent);
5452
5877
  results2.push(...configuredResults);
5453
5878
  const exitCode2 = displayResults(results2, context);
5454
5879
  return { exitCode: exitCode2 };
5455
5880
  }
5456
5881
  const checkPromises = [];
5457
5882
  if (hasDustDir) {
5458
- checkPromises.push(runValidationCheck(dependencies));
5883
+ checkPromises.push(runValidationCheck(dependencies, context.emitEvent));
5459
5884
  }
5460
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
5885
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner, context.emitEvent));
5461
5886
  const promiseResults = await Promise.all(checkPromises);
5462
5887
  const results = [];
5463
5888
  for (const result of promiseResults) {
@@ -5638,6 +6063,16 @@ async function init(dependencies) {
5638
6063
 
5639
6064
  // lib/cli/commands/list.ts
5640
6065
  import { basename as basename2 } from "node:path";
6066
+ function workflowTypeToStatus(type) {
6067
+ switch (type) {
6068
+ case "refine":
6069
+ return "refining";
6070
+ case "decompose-idea":
6071
+ return "decomposing";
6072
+ case "shelve":
6073
+ return "shelving";
6074
+ }
6075
+ }
5641
6076
  var VALID_TYPES = ["tasks", "ideas", "principles", "facts"];
5642
6077
  var SECTION_HEADERS = {
5643
6078
  tasks: "\uD83D\uDCCB Tasks",
@@ -5712,6 +6147,7 @@ async function list(dependencies) {
5712
6147
  return { exitCode: 1 };
5713
6148
  }
5714
6149
  const specificTypeRequested = commandArguments.length > 0;
6150
+ const workflowTasks = typesToList.includes("ideas") && fileSystem.exists(dustPath) ? await findAllWorkflowTasks(fileSystem, dustPath) : null;
5715
6151
  for (const type of typesToList) {
5716
6152
  const dirPath = `${dustPath}/${type}`;
5717
6153
  const dirExists = fileSystem.exists(dirPath);
@@ -5726,6 +6162,13 @@ async function list(dependencies) {
5726
6162
  context.stdout(`No ${type} found.`);
5727
6163
  context.stdout("");
5728
6164
  }
6165
+ if (type === "facts") {
6166
+ context.emitEvent?.({ type: "facts-listed", facts: [] });
6167
+ } else if (type === "ideas") {
6168
+ context.emitEvent?.({ type: "ideas-listed", ideas: [] });
6169
+ } else if (type === "principles") {
6170
+ context.emitEvent?.({ type: "principles-listed", principles: [] });
6171
+ }
5729
6172
  continue;
5730
6173
  }
5731
6174
  context.stdout(SECTION_HEADERS[type]);
@@ -5740,16 +6183,26 @@ async function list(dependencies) {
5740
6183
  context.stdout("");
5741
6184
  }
5742
6185
  }
6186
+ const collectedItems = [];
5743
6187
  for (const file of mdFiles) {
5744
6188
  const filePath = `${dirPath}/${file}`;
5745
6189
  const content = await fileSystem.readFile(filePath);
5746
6190
  const title = extractTitle(content);
5747
6191
  const openingSentence = extractOpeningSentence(content);
5748
6192
  const relativePath = `.dust/${type}/${file}`;
6193
+ const slug = file.replace(".md", "");
6194
+ const displayTitle = title || slug;
6195
+ if (type === "ideas") {
6196
+ const workflowTask = workflowTasks?.workflowTasksByIdeaSlug.get(slug);
6197
+ const status = workflowTask ? workflowTypeToStatus(workflowTask.type) : "draft";
6198
+ collectedItems.push({ path: relativePath, title: displayTitle, status });
6199
+ } else if (type === "facts" || type === "principles") {
6200
+ collectedItems.push({ path: relativePath, title: displayTitle });
6201
+ }
5749
6202
  if (title) {
5750
6203
  context.stdout(`${colors.bold}# ${title}${colors.reset}`);
5751
6204
  } else {
5752
- context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
6205
+ context.stdout(`${colors.bold}# ${slug}${colors.reset}`);
5753
6206
  }
5754
6207
  if (openingSentence) {
5755
6208
  context.stdout(`${colors.dim}${openingSentence}${colors.reset}`);
@@ -5757,130 +6210,28 @@ async function list(dependencies) {
5757
6210
  context.stdout(`${colors.cyan}→ ${relativePath}${colors.reset}`);
5758
6211
  context.stdout("");
5759
6212
  }
5760
- }
5761
- return { exitCode: 0 };
5762
- }
5763
-
5764
- // lib/codex/spawn-codex.ts
5765
- import { spawn as nodeSpawn5 } from "node:child_process";
5766
- import { createInterface as nodeCreateInterface2 } from "node:readline";
5767
- var debug2 = createLogger("dust.codex.spawn-codex");
5768
- var defaultDependencies2 = {
5769
- spawn: nodeSpawn5,
5770
- createInterface: nodeCreateInterface2
5771
- };
5772
- async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
5773
- const { cwd, env, signal } = options;
5774
- const codexArguments = ["exec", prompt, "--json", "--yolo"];
5775
- if (cwd) {
5776
- codexArguments.push("--cd", cwd);
5777
- }
5778
- const proc = dependencies.spawn("codex", codexArguments, {
5779
- stdio: ["ignore", "pipe", "pipe"],
5780
- env: { ...process.env, ...env }
5781
- });
5782
- if (!proc.stdout) {
5783
- throw new Error("Failed to get stdout from codex process");
5784
- }
5785
- let stderrOutput = "";
5786
- proc.stderr?.on("data", (data) => {
5787
- stderrOutput += data.toString();
5788
- });
5789
- const closePromise = new Promise((resolve3, reject) => {
5790
- proc.on("close", (code) => {
5791
- if (code === 0 || code === null)
5792
- resolve3();
5793
- else {
5794
- const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
5795
- reject(new Error(errMsg));
5796
- }
5797
- });
5798
- proc.on("error", reject);
5799
- });
5800
- const abortHandler = () => {
5801
- if (!proc.killed) {
5802
- proc.kill();
5803
- }
5804
- };
5805
- if (signal?.aborted) {
5806
- abortHandler();
5807
- } else if (signal) {
5808
- signal.addEventListener("abort", abortHandler, { once: true });
5809
- }
5810
- const rl = dependencies.createInterface({ input: proc.stdout });
5811
- try {
5812
- for await (const line of rl) {
5813
- if (!line.trim())
5814
- continue;
5815
- try {
5816
- yield JSON.parse(line);
5817
- } catch {
5818
- debug2("Skipping malformed JSON line: %s", line.slice(0, 200));
5819
- }
5820
- }
5821
- await closePromise;
5822
- } finally {
5823
- signal?.removeEventListener("abort", abortHandler);
5824
- rl.close?.();
5825
- }
5826
- }
5827
-
5828
- // lib/codex/event-parser.ts
5829
- function* parseCodexRawEvent(raw) {
5830
- if (raw.type !== "item.completed")
5831
- return;
5832
- const item = raw.item;
5833
- if (!item)
5834
- return;
5835
- if (item.type === "agent_message" && typeof item.text === "string") {
5836
- yield { type: "text_delta", text: `${item.text}
5837
- ` };
5838
- } else if (item.type === "command_execution" && typeof item.command === "string") {
5839
- yield {
5840
- type: "tool_use",
5841
- id: typeof item.id === "string" ? item.id : "",
5842
- name: "command_execution",
5843
- input: { command: item.command }
5844
- };
5845
- if (typeof item.aggregated_output === "string") {
5846
- yield {
5847
- type: "tool_result",
5848
- toolUseId: typeof item.id === "string" ? item.id : "",
5849
- content: item.aggregated_output || `(exit code: ${item.exit_code ?? "unknown"})`
5850
- };
5851
- }
5852
- }
5853
- }
5854
-
5855
- // lib/codex/streamer.ts
5856
- async function streamCodexEvents(events, sink, onRawEvent) {
5857
- let hadTextOutput = false;
5858
- for await (const raw of events) {
5859
- onRawEvent?.(raw);
5860
- for (const event of parseCodexRawEvent(raw)) {
5861
- processEvent(event, sink, { hadTextOutput });
5862
- if (event.type === "text_delta") {
5863
- hadTextOutput = true;
5864
- } else if (event.type === "tool_use") {
5865
- hadTextOutput = false;
5866
- }
6213
+ if (type === "facts") {
6214
+ context.emitEvent?.({
6215
+ type: "facts-listed",
6216
+ facts: collectedItems.map((i) => ({ path: i.path, title: i.title }))
6217
+ });
6218
+ } else if (type === "ideas") {
6219
+ context.emitEvent?.({
6220
+ type: "ideas-listed",
6221
+ ideas: collectedItems.map((i) => ({
6222
+ path: i.path,
6223
+ title: i.title,
6224
+ status: i.status ?? "draft"
6225
+ }))
6226
+ });
6227
+ } else if (type === "principles") {
6228
+ context.emitEvent?.({
6229
+ type: "principles-listed",
6230
+ principles: collectedItems.map((i) => ({ path: i.path, title: i.title }))
6231
+ });
5867
6232
  }
5868
6233
  }
5869
- }
5870
-
5871
- // lib/codex/run.ts
5872
- var defaultRunnerDependencies2 = {
5873
- spawnCodex,
5874
- createStdoutSink,
5875
- streamCodexEvents
5876
- };
5877
- async function run2(prompt, options = {}, dependencies = defaultRunnerDependencies2) {
5878
- const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
5879
- const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
5880
- const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
5881
- const events = dependencies.spawnCodex(prompt, spawnOptions);
5882
- const sink = dependencies.createStdoutSink();
5883
- await dependencies.streamCodexEvents(events, sink, onRawEvent);
6234
+ return { exitCode: 0 };
5884
6235
  }
5885
6236
 
5886
6237
  // lib/cli/commands/loop-codex.ts
@@ -6137,8 +6488,8 @@ function parseGitDiffNameStatus(output) {
6137
6488
  const parts = line.split("\t");
6138
6489
  if (parts.length >= 2) {
6139
6490
  const statusChar = parts[0].charAt(0);
6140
- const path = parts.length > 2 ? parts[2] : parts[1];
6141
- changes.push({ status: statusChar, path });
6491
+ const path3 = parts.length > 2 ? parts[2] : parts[1];
6492
+ changes.push({ status: statusChar, path: path3 });
6142
6493
  }
6143
6494
  }
6144
6495
  return changes;
@@ -6199,12 +6550,12 @@ async function getUncommittedFiles(cwd, gitRunner) {
6199
6550
  `).filter((line) => line.length > 0);
6200
6551
  for (const line of lines) {
6201
6552
  if (line.length > 3) {
6202
- const path = line.substring(3);
6203
- const arrowIndex = path.indexOf(" -> ");
6553
+ const path3 = line.substring(3);
6554
+ const arrowIndex = path3.indexOf(" -> ");
6204
6555
  if (arrowIndex !== -1) {
6205
- files.push(path.substring(arrowIndex + 4));
6556
+ files.push(path3.substring(arrowIndex + 4));
6206
6557
  } else {
6207
- files.push(path);
6558
+ files.push(path3);
6208
6559
  }
6209
6560
  }
6210
6561
  }
@@ -6355,24 +6706,24 @@ async function main(options) {
6355
6706
  function createFileSystem(primitives) {
6356
6707
  return {
6357
6708
  exists: primitives.existsSync,
6358
- isDirectory: (path) => {
6709
+ isDirectory: (path3) => {
6359
6710
  try {
6360
- return primitives.statSync(path).isDirectory();
6711
+ return primitives.statSync(path3).isDirectory();
6361
6712
  } catch {
6362
6713
  return false;
6363
6714
  }
6364
6715
  },
6365
- readFile: (path) => primitives.readFile(path, "utf-8"),
6366
- writeFile: (path, content, options) => primitives.writeFile(path, content, {
6716
+ readFile: (path3) => primitives.readFile(path3, "utf-8"),
6717
+ writeFile: (path3, content, options) => primitives.writeFile(path3, content, {
6367
6718
  encoding: "utf-8",
6368
6719
  flag: options?.flag
6369
6720
  }),
6370
- mkdir: async (path, options) => {
6371
- await primitives.mkdir(path, options);
6721
+ mkdir: async (path3, options) => {
6722
+ await primitives.mkdir(path3, options);
6372
6723
  },
6373
- getFileCreationTime: (path) => primitives.statSync(path).birthtimeMs,
6374
- readdir: (path) => primitives.readdir(path),
6375
- chmod: (path, mode) => primitives.chmod(path, mode),
6724
+ getFileCreationTime: (path3) => primitives.statSync(path3).birthtimeMs,
6725
+ readdir: (path3) => primitives.readdir(path3),
6726
+ chmod: (path3, mode) => primitives.chmod(path3, mode),
6376
6727
  rename: (oldPath, newPath) => primitives.rename(oldPath, newPath)
6377
6728
  };
6378
6729
  }
@@ -6396,7 +6747,8 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
6396
6747
  cwd: processPrimitives.cwd(),
6397
6748
  stdout: consolePrimitives.log,
6398
6749
  stdoutInline: consolePrimitives.write,
6399
- stderr: consolePrimitives.error
6750
+ stderr: consolePrimitives.error,
6751
+ emitEvent: consolePrimitives.emitEvent
6400
6752
  },
6401
6753
  fileSystem,
6402
6754
  glob,
@@ -6406,7 +6758,12 @@ async function wireEntry(fsPrimitives, processPrimitives, consolePrimitives) {
6406
6758
  }
6407
6759
 
6408
6760
  // lib/cli/run.ts
6409
- await wireEntry({ existsSync, statSync: statSync3, readFile: readFile3, writeFile: writeFile3, mkdir: mkdir3, readdir: readdir3, chmod: chmod3, rename }, {
6761
+ var eventsFd = process.env.DUST_EVENTS_FD ? Number.parseInt(process.env.DUST_EVENTS_FD, 10) : undefined;
6762
+ var emitEvent = eventsFd !== undefined && !Number.isNaN(eventsFd) ? createEventEmitter((message) => {
6763
+ writeSync(eventsFd, `${JSON.stringify(message)}
6764
+ `);
6765
+ }) : undefined;
6766
+ await wireEntry({ existsSync: existsSync2, statSync: statSync3, readFile: readFile3, writeFile: writeFile3, mkdir: mkdir3, readdir: readdir3, chmod: chmod3, rename }, {
6410
6767
  argv: process.argv,
6411
6768
  cwd: () => process.cwd(),
6412
6769
  exit: (code) => {
@@ -6417,5 +6774,6 @@ await wireEntry({ existsSync, statSync: statSync3, readFile: readFile3, writeFil
6417
6774
  write: (message) => {
6418
6775
  process.stdout.write(message);
6419
6776
  },
6420
- error: console.error
6777
+ error: console.error,
6778
+ emitEvent
6421
6779
  });