@joshski/dust 0.1.91 → 0.1.93

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,6 +40,7 @@ export interface ArtifactsRepository {
40
40
  createRefineIdeaTask(options: {
41
41
  ideaSlug: string;
42
42
  description?: string;
43
+ openQuestionResponses?: OpenQuestionResponse[];
43
44
  dustCommand?: string;
44
45
  }): Promise<CreateIdeaTransitionTaskResult>;
45
46
  createDecomposeIdeaTask(options: DecomposeIdeaOptions & {
@@ -45,7 +45,7 @@ export interface DecomposeIdeaOptions {
45
45
  description?: string;
46
46
  openQuestionResponses?: OpenQuestionResponse[];
47
47
  }
48
- export declare function createRefineIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string, dustCommand?: string): Promise<CreateIdeaTransitionTaskResult>;
48
+ export declare function createRefineIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string, openQuestionResponses?: OpenQuestionResponse[], dustCommand?: string): Promise<CreateIdeaTransitionTaskResult>;
49
49
  export declare function decomposeIdea(fileSystem: FileSystem, dustPath: string, options: DecomposeIdeaOptions, dustCommand?: string): Promise<CreateIdeaTransitionTaskResult>;
50
50
  export declare function createShelveIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string, _dustCommand?: string): Promise<CreateIdeaTransitionTaskResult>;
51
51
  export declare function createIdeaTask(fileSystem: FileSystem, dustPath: string, options: {
package/dist/artifacts.js CHANGED
@@ -460,14 +460,17 @@ async function createIdeaTransitionTask(fileSystem, dustPath, workflowType, pref
460
460
  await fileSystem.writeFile(filePath, content);
461
461
  return { filePath };
462
462
  }
463
- async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description, dustCommand) {
463
+ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description, openQuestionResponses, dustCommand) {
464
464
  const cmd = dustCommand ?? "dust";
465
465
  return createIdeaTransitionTask(fileSystem, dustPath, "refine-idea", "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.`, [
466
466
  "Idea is thoroughly researched with relevant codebase context",
467
467
  "Open questions are added for any ambiguous or underspecified aspects",
468
468
  "Open questions follow the required heading format and focus on high-value decisions",
469
469
  "Idea file is updated with findings"
470
- ], "Refines Idea", { description });
470
+ ], "Refines Idea", {
471
+ description,
472
+ resolvedQuestions: openQuestionResponses
473
+ });
471
474
  }
472
475
  async function decomposeIdea(fileSystem, dustPath, options, dustCommand) {
473
476
  const cmd = dustCommand ?? "dust";
@@ -632,7 +635,7 @@ function buildArtifactsRepository(fileSystem, dustPath) {
632
635
  return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).sort();
633
636
  },
634
637
  async createRefineIdeaTask(options) {
635
- return createRefineIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description, options.dustCommand);
638
+ return createRefineIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description, options.openQuestionResponses, options.dustCommand);
636
639
  },
637
640
  async createDecomposeIdeaTask(options) {
638
641
  return decomposeIdea(fileSystem, dustPath, options, options.dustCommand);
package/dist/dust.js CHANGED
@@ -500,7 +500,7 @@ async function loadSettings(cwd, fileSystem) {
500
500
  }
501
501
 
502
502
  // lib/version.ts
503
- var DUST_VERSION = "0.1.91";
503
+ var DUST_VERSION = "0.1.93";
504
504
 
505
505
  // lib/session.ts
506
506
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -2072,6 +2072,7 @@ import { spawn as nodeSpawn5 } from "node:child_process";
2072
2072
  import { accessSync, statSync } from "node:fs";
2073
2073
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2074
2074
  import { homedir as homedir2 } from "node:os";
2075
+ import { basename as basename2, resolve } from "node:path";
2075
2076
 
2076
2077
  // lib/bucket/auth.ts
2077
2078
  import { join as join5 } from "node:path";
@@ -2562,6 +2563,25 @@ function selectAgentCapabilities(input) {
2562
2563
  }
2563
2564
  return agents;
2564
2565
  }
2566
+ async function fetchCodexModelsFromApi(apiKey) {
2567
+ try {
2568
+ const response = await fetch("https://api.openai.com/v1/models", {
2569
+ headers: { Authorization: `Bearer ${apiKey}` }
2570
+ });
2571
+ if (!response.ok) {
2572
+ log2(`OpenAI models API returned ${response.status}`);
2573
+ return [];
2574
+ }
2575
+ const body = await response.json();
2576
+ if (!body.data || !Array.isArray(body.data)) {
2577
+ return [];
2578
+ }
2579
+ return body.data.map((m) => m.id).filter((id) => id.includes("codex")).sort();
2580
+ } catch (error) {
2581
+ log2(`OpenAI models API fetch failed: ${error instanceof Error ? error.message : String(error)}`);
2582
+ return [];
2583
+ }
2584
+ }
2565
2585
  async function discoverAgentCapabilities(dependencies) {
2566
2586
  const [claudeVersionProbe, codexVersionProbe] = await Promise.all([
2567
2587
  probeCommand(dependencies.spawn, "claude", ["--version"]),
@@ -2569,10 +2589,18 @@ async function discoverAgentCapabilities(dependencies) {
2569
2589
  ]);
2570
2590
  let codexModelsProbe = null;
2571
2591
  if (codexVersionProbe.success) {
2572
- codexModelsProbe = await probeCommand(dependencies.spawn, "codex", [
2573
- "models",
2574
- "--json"
2575
- ]);
2592
+ const getEnv = dependencies.getEnv ?? ((name) => process.env[name]);
2593
+ const apiKey = getEnv("OPENAI_API_KEY");
2594
+ if (apiKey) {
2595
+ const fetcher = dependencies.fetchCodexModelsFromApi ?? fetchCodexModelsFromApi;
2596
+ const models = await fetcher(apiKey);
2597
+ if (models.length > 0) {
2598
+ codexModelsProbe = {
2599
+ success: true,
2600
+ stdout: JSON.stringify(models)
2601
+ };
2602
+ }
2603
+ }
2576
2604
  }
2577
2605
  return {
2578
2606
  type: "agent-capabilities",
@@ -5600,7 +5628,7 @@ function createDefaultBucketDependencies() {
5600
5628
  getTerminalSize: defaultGetTerminalSize,
5601
5629
  writeStdout: defaultWriteStdout,
5602
5630
  isTTY: process.stdout.isTTY ?? false,
5603
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5631
+ sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
5604
5632
  getReposDir: () => getReposDir(process.env, homedir2()),
5605
5633
  auth: {
5606
5634
  createServer: createLocalServer,
@@ -5744,8 +5772,8 @@ function createTUIContext(state, context, useTUI) {
5744
5772
  function waitForConnection(token, bucketDeps) {
5745
5773
  const wsUrl = getWebSocketUrl();
5746
5774
  const ws = bucketDeps.createWebSocket(wsUrl, token);
5747
- return new Promise((resolve, reject) => {
5748
- ws.onopen = () => resolve(ws);
5775
+ return new Promise((resolve2, reject) => {
5776
+ ws.onopen = () => resolve2(ws);
5749
5777
  ws.onerror = (error) => reject(new Error(error.message));
5750
5778
  ws.onclose = (event) => reject(new Error(`Connection closed (code ${event.code})`));
5751
5779
  });
@@ -6140,24 +6168,51 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6140
6168
  } catch {
6141
6169
  state.ui.connectedHost = wsUrl;
6142
6170
  }
6171
+ function findRepoPathByRepositoryId(repositories, repositoryId) {
6172
+ for (const repoState of repositories.values()) {
6173
+ if (repoState.repository.id === repositoryId) {
6174
+ return repoState.path;
6175
+ }
6176
+ }
6177
+ return;
6178
+ }
6143
6179
  let tuiHandle;
6144
6180
  let cleanupKeypress;
6145
6181
  let cleanupSignals;
6146
- const forwardToolExecution = (request) => {
6182
+ const forwardToolExecution = async (request) => {
6147
6183
  const ws = state.ws;
6148
6184
  if (!ws || ws.readyState !== WS_OPEN) {
6149
6185
  log10(`tool execution rejected: ${request.toolName} (WebSocket not connected)`);
6150
- return Promise.resolve({
6186
+ return {
6151
6187
  status: "error",
6152
6188
  error: "Bucket session is not connected"
6153
- });
6189
+ };
6154
6190
  }
6155
6191
  const requestId = crypto.randomUUID();
6192
+ const repoPath = findRepoPathByRepositoryId(state.repositories, Number(request.repositoryId));
6156
6193
  const toolDef = state.tools.find((t) => t.name === request.toolName);
6157
6194
  const namedArgs = {};
6158
6195
  if (toolDef) {
6159
6196
  for (let i = 0;i < toolDef.parameters.length && i < request.arguments.length; i++) {
6160
- namedArgs[toolDef.parameters[i].name] = request.arguments[i];
6197
+ const param = toolDef.parameters[i];
6198
+ const value = request.arguments[i];
6199
+ if (param.type === "file" && typeof value === "string") {
6200
+ const filePath = repoPath ? resolve(repoPath, value) : value;
6201
+ try {
6202
+ const data = await readFile(filePath);
6203
+ namedArgs[param.name] = {
6204
+ filename: basename2(value),
6205
+ data: data.toString("base64")
6206
+ };
6207
+ } catch {
6208
+ return {
6209
+ status: "error",
6210
+ error: `Unable to read file: ${value}`
6211
+ };
6212
+ }
6213
+ } else {
6214
+ namedArgs[param.name] = value;
6215
+ }
6161
6216
  }
6162
6217
  } else {
6163
6218
  for (let i = 0;i < request.arguments.length; i++) {
@@ -6172,14 +6227,14 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6172
6227
  arguments: namedArgs
6173
6228
  };
6174
6229
  log10(`forwarding tool execution: ${request.toolName} requestId=${requestId}`);
6175
- return new Promise((resolve, reject) => {
6230
+ return new Promise((resolve2, reject) => {
6176
6231
  const timeoutId = setTimeout(() => {
6177
6232
  pendingToolExecutions.delete(requestId);
6178
6233
  log10(`tool execution timed out: ${request.toolName} requestId=${requestId}`);
6179
6234
  reject(new Error("Timed out waiting for tool execution result"));
6180
6235
  }, 30000);
6181
6236
  pendingToolExecutions.set(requestId, {
6182
- resolve,
6237
+ resolve: resolve2,
6183
6238
  reject,
6184
6239
  timeoutId
6185
6240
  });
@@ -6197,10 +6252,10 @@ async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDepend
6197
6252
  if (useTUI) {
6198
6253
  tuiHandle = setupTUI(state, bucketDeps);
6199
6254
  }
6200
- await new Promise((resolve) => {
6255
+ await new Promise((resolve2) => {
6201
6256
  const doShutdown = async () => {
6202
6257
  await shutdown(state, bucketDeps, context);
6203
- resolve();
6258
+ resolve2();
6204
6259
  };
6205
6260
  const onKey = createKeypressHandler(useTUI, state, () => {
6206
6261
  doShutdown();
@@ -6405,7 +6460,7 @@ function createGitRunner(spawnFn) {
6405
6460
  }
6406
6461
  var defaultGitRunner = createGitRunner(spawn);
6407
6462
  function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, timeoutMs) {
6408
- return new Promise((resolve) => {
6463
+ return new Promise((resolve2) => {
6409
6464
  const proc = spawnFn(command, commandArguments, { cwd, shell });
6410
6465
  const chunks = [];
6411
6466
  let resolved = false;
@@ -6417,7 +6472,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
6417
6472
  proc.stdout?.destroy();
6418
6473
  proc.stderr?.destroy();
6419
6474
  proc.unref();
6420
- resolve({
6475
+ resolve2({
6421
6476
  exitCode: 1,
6422
6477
  output: chunks.join(""),
6423
6478
  timedOut: true
@@ -6435,14 +6490,14 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
6435
6490
  return;
6436
6491
  if (timer !== undefined)
6437
6492
  clearTimeout(timer);
6438
- resolve({ exitCode: code ?? 1, output: chunks.join("") });
6493
+ resolve2({ exitCode: code ?? 1, output: chunks.join("") });
6439
6494
  });
6440
6495
  proc.on("error", (error) => {
6441
6496
  if (resolved)
6442
6497
  return;
6443
6498
  if (timer !== undefined)
6444
6499
  clearTimeout(timer);
6445
- resolve({ exitCode: 1, output: error.message });
6500
+ resolve2({ exitCode: 1, output: error.message });
6446
6501
  });
6447
6502
  });
6448
6503
  }
@@ -6903,7 +6958,7 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
6903
6958
  }
6904
6959
 
6905
6960
  // lib/lint/validators/link-validator.ts
6906
- import { dirname as dirname3, resolve } from "node:path";
6961
+ import { dirname as dirname3, resolve as resolve2 } from "node:path";
6907
6962
  var SEMANTIC_RULES = [
6908
6963
  {
6909
6964
  section: "## Principles",
@@ -6936,7 +6991,7 @@ function validateLinks(filePath, content, fileSystem) {
6936
6991
  });
6937
6992
  } else {
6938
6993
  const targetPath = linkTarget.split("#")[0];
6939
- const resolvedPath = resolve(fileDir, targetPath);
6994
+ const resolvedPath = resolve2(fileDir, targetPath);
6940
6995
  if (!fileSystem.exists(resolvedPath)) {
6941
6996
  violations.push({
6942
6997
  file: filePath,
@@ -6989,7 +7044,7 @@ function validateSemanticLinks(filePath, content) {
6989
7044
  continue;
6990
7045
  }
6991
7046
  const targetPath = linkTarget.split("#")[0];
6992
- const resolvedPath = resolve(fileDir, targetPath);
7047
+ const resolvedPath = resolve2(fileDir, targetPath);
6993
7048
  if (!resolvedPath.includes(rule.requiredPath)) {
6994
7049
  violations.push({
6995
7050
  file: filePath,
@@ -7040,7 +7095,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
7040
7095
  continue;
7041
7096
  }
7042
7097
  const targetPath = linkTarget.split("#")[0];
7043
- const resolvedPath = resolve(fileDir, targetPath);
7098
+ const resolvedPath = resolve2(fileDir, targetPath);
7044
7099
  if (!resolvedPath.includes("/.dust/principles/")) {
7045
7100
  violations.push({
7046
7101
  file: filePath,
@@ -7055,7 +7110,7 @@ function validatePrincipleHierarchyLinks(filePath, content) {
7055
7110
  }
7056
7111
 
7057
7112
  // lib/lint/validators/principle-hierarchy.ts
7058
- import { dirname as dirname4, resolve as resolve2 } from "node:path";
7113
+ import { dirname as dirname4, resolve as resolve3 } from "node:path";
7059
7114
  var REQUIRED_PRINCIPLE_HEADINGS = ["## Parent Principle", "## Sub-Principles"];
7060
7115
  function validatePrincipleHierarchySections(filePath, content) {
7061
7116
  const violations = [];
@@ -7090,7 +7145,7 @@ function extractPrincipleRelationships(filePath, content) {
7090
7145
  const linkTarget = match[2];
7091
7146
  if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
7092
7147
  const targetPath = linkTarget.split("#")[0];
7093
- const resolvedPath = resolve2(fileDir, targetPath);
7148
+ const resolvedPath = resolve3(fileDir, targetPath);
7094
7149
  if (resolvedPath.includes("/.dust/principles/")) {
7095
7150
  if (currentSection === "## Parent Principle") {
7096
7151
  parentPrinciples.push(resolvedPath);
@@ -7742,7 +7797,7 @@ async function init(dependencies) {
7742
7797
  }
7743
7798
 
7744
7799
  // lib/cli/commands/list.ts
7745
- import { basename as basename2 } from "node:path";
7800
+ import { basename as basename3 } from "node:path";
7746
7801
  function workflowTypeToStatus(type) {
7747
7802
  switch (type) {
7748
7803
  case "refine-idea":
@@ -7775,7 +7830,7 @@ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
7775
7830
  const filePath = `${principlesPath}/${file}`;
7776
7831
  const content = await fileSystem.readFile(filePath);
7777
7832
  relationships.push(extractPrincipleRelationships(filePath, content));
7778
- const title = extractTitle(content) || basename2(file, ".md");
7833
+ const title = extractTitle(content) || basename3(file, ".md");
7779
7834
  titleMap.set(filePath, title);
7780
7835
  }
7781
7836
  const relMap = new Map;
@@ -7793,7 +7848,7 @@ async function buildPrincipleHierarchy(principlesPath, fileSystem) {
7793
7848
  }
7794
7849
  return {
7795
7850
  filePath,
7796
- title: titleMap.get(filePath) || basename2(filePath, ".md"),
7851
+ title: titleMap.get(filePath) || basename3(filePath, ".md"),
7797
7852
  children
7798
7853
  };
7799
7854
  }
@@ -7812,7 +7867,12 @@ function renderHierarchy(nodes, output, prefix = "") {
7812
7867
  }
7813
7868
  }
7814
7869
  async function list(dependencies) {
7815
- const { arguments: commandArguments, context, fileSystem } = dependencies;
7870
+ const {
7871
+ arguments: commandArguments,
7872
+ context,
7873
+ fileSystem,
7874
+ settings
7875
+ } = dependencies;
7816
7876
  const dustPath = `${context.cwd}/.dust`;
7817
7877
  const colors = getColors();
7818
7878
  if (!fileSystem.exists(dustPath)) {
@@ -7827,6 +7887,7 @@ async function list(dependencies) {
7827
7887
  return { exitCode: 1 };
7828
7888
  }
7829
7889
  const specificTypeRequested = commandArguments.length > 0;
7890
+ const showTaskCreationHint = specificTypeRequested && typesToList.length === 1 && typesToList[0] === "tasks";
7830
7891
  const workflowTasks = typesToList.includes("ideas") && fileSystem.exists(dustPath) ? await findAllWorkflowTasks(fileSystem, dustPath) : null;
7831
7892
  for (const type of typesToList) {
7832
7893
  const dirPath = `${dustPath}/${type}`;
@@ -7911,6 +7972,11 @@ async function list(dependencies) {
7911
7972
  });
7912
7973
  }
7913
7974
  }
7975
+ if (showTaskCreationHint) {
7976
+ context.stdout("➕ Add a New Task");
7977
+ context.stdout("");
7978
+ context.stdout(`Run \`${settings.dustCommand} new task\``);
7979
+ }
7914
7980
  return { exitCode: 0 };
7915
7981
  }
7916
7982
 
@@ -8113,7 +8179,7 @@ function newTaskInstructions(vars) {
8113
8179
  steps.push(" - The goal is a task description with minimal ambiguity at implementation time");
8114
8180
  steps.push("4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)");
8115
8181
  steps.push("5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)");
8116
- steps.push('6. Write a comprehensive description starting with an imperative opening sentence (e.g., "Add caching to the API layer." not "This task adds caching."). Include technical details and references to relevant files.');
8182
+ steps.push('6. Write a comprehensive description starting with an imperative opening sentence (e.g., "Add caching to the API layer." not "This task adds caching."). The opening sentence must be 150 characters or fewer. Include technical details and references to relevant files.');
8117
8183
  steps.push("7. Add a `## Principles` section with links to relevant principles this task supports (e.g., `- [Principle Name](../principles/principle-name.md)`)");
8118
8184
  steps.push("8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers");
8119
8185
  steps.push("9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {