@joshski/dust 0.1.21 → 0.1.23

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.
@@ -0,0 +1,59 @@
1
+ // lib/artifacts/idea-transition-task.ts
2
+ var IDEA_TRANSITION_PREFIXES = [
3
+ "Refine Idea: ",
4
+ "Create Task From Idea: ",
5
+ "Shelve Idea: "
6
+ ];
7
+ var TRANSITION_PREFIX_MAP = {
8
+ "refine-idea": "Refine Idea: ",
9
+ "create-task-from-idea": "Create Task From Idea: ",
10
+ "shelve-idea": "Shelve Idea: "
11
+ };
12
+ function titleToFilename(title) {
13
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
14
+ }
15
+ async function createIdeaTransitionTask(fileSystem, dustPath, input) {
16
+ const ideaPath = `${dustPath}/ideas/${input.ideaSlug}.md`;
17
+ if (!fileSystem.exists(ideaPath)) {
18
+ throw new Error(`Idea not found: "${input.ideaSlug}" (expected file at ${ideaPath})`);
19
+ }
20
+ const ideaContent = await fileSystem.readFile(ideaPath);
21
+ const ideaTitleMatch = ideaContent.match(/^#\s+(.+)$/m);
22
+ if (!ideaTitleMatch) {
23
+ throw new Error(`Idea file has no title: ${ideaPath}`);
24
+ }
25
+ const ideaTitle = ideaTitleMatch[1].trim();
26
+ const prefix = TRANSITION_PREFIX_MAP[input.transition];
27
+ const taskTitle = `${prefix}${ideaTitle}`;
28
+ const filename = titleToFilename(taskTitle);
29
+ const filePath = `${dustPath}/tasks/${filename}`;
30
+ const goalsSection = input.goals.length > 0 ? input.goals.map((slug) => `- [${slug}](../goals/${slug}.md)`).join(`
31
+ `) : "(none)";
32
+ const blockedBySection = input.blockedBy.length > 0 ? input.blockedBy.map((slug) => `- [${slug}](../tasks/${slug}.md)`).join(`
33
+ `) : "(none)";
34
+ const definitionOfDoneSection = input.definitionOfDone.map((item) => `- [ ] ${item}`).join(`
35
+ `);
36
+ const content = `# ${taskTitle}
37
+
38
+ ${input.openingSentence}
39
+
40
+ ## Goals
41
+
42
+ ${goalsSection}
43
+
44
+ ## Blocked By
45
+
46
+ ${blockedBySection}
47
+
48
+ ## Definition of Done
49
+
50
+ ${definitionOfDoneSection}
51
+ `;
52
+ await fileSystem.writeFile(filePath, content);
53
+ return { filePath };
54
+ }
55
+ export {
56
+ titleToFilename,
57
+ createIdeaTransitionTask,
58
+ IDEA_TRANSITION_PREFIXES
59
+ };
package/dist/dust.js CHANGED
@@ -108,6 +108,9 @@ function loadTemplate(name, variables = {}) {
108
108
  return content;
109
109
  }
110
110
 
111
+ // lib/cli/commands/agent-shared.ts
112
+ import { join as join4 } from "node:path";
113
+
111
114
  // lib/agents/detection.ts
112
115
  function detectAgent(env = process.env) {
113
116
  if (env.CLAUDECODE) {
@@ -252,6 +255,18 @@ ${newHookContent}
252
255
  }
253
256
 
254
257
  // lib/cli/commands/agent-shared.ts
258
+ async function loadAgentInstructions(cwd, fileSystem, agentType) {
259
+ const instructionsPath = join4(cwd, ".dust", "config", "agents", `${agentType}.md`);
260
+ if (!fileSystem.exists(instructionsPath)) {
261
+ return "";
262
+ }
263
+ try {
264
+ const content = await fileSystem.readFile(instructionsPath);
265
+ return content.trim();
266
+ } catch {
267
+ return "";
268
+ }
269
+ }
255
270
  function templateVariables(settings, hooksInstalled, env = process.env) {
256
271
  const agent = detectAgent(env);
257
272
  return {
@@ -261,6 +276,17 @@ function templateVariables(settings, hooksInstalled, env = process.env) {
261
276
  isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : ""
262
277
  };
263
278
  }
279
+ async function templateVariablesWithInstructions(cwd, fileSystem, settings, hooksInstalled, env = process.env) {
280
+ const agent = detectAgent(env);
281
+ const agentInstructions = await loadAgentInstructions(cwd, fileSystem, agent.type);
282
+ return {
283
+ bin: settings.dustCommand,
284
+ agentName: agent.name,
285
+ hooksInstalled: hooksInstalled ? "true" : "false",
286
+ isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : "",
287
+ agentInstructions
288
+ };
289
+ }
264
290
  async function manageGitHooks(dependencies) {
265
291
  const { context, fileSystem, settings } = dependencies;
266
292
  const hooks = createHooksManager(context.cwd, fileSystem, settings);
@@ -281,19 +307,59 @@ async function manageGitHooks(dependencies) {
281
307
 
282
308
  // lib/cli/commands/agent.ts
283
309
  async function agent(dependencies) {
284
- const { context, settings } = dependencies;
310
+ const { context, fileSystem, settings } = dependencies;
285
311
  const hooksInstalled = await manageGitHooks(dependencies);
286
- const vars = templateVariables(settings, hooksInstalled);
312
+ const vars = await templateVariablesWithInstructions(context.cwd, fileSystem, settings, hooksInstalled);
287
313
  context.stdout(loadTemplate("agent-greeting", vars));
288
314
  return { exitCode: 0 };
289
315
  }
290
316
 
291
- // lib/cli/commands/check.ts
317
+ // lib/cli/process-runner.ts
292
318
  import { spawn } from "node:child_process";
319
+ function createShellRunner(spawnFn) {
320
+ return {
321
+ run: (command, cwd) => runBufferedProcess(spawnFn, command, [], cwd, true)
322
+ };
323
+ }
324
+ var defaultShellRunner = createShellRunner(spawn);
325
+ function createGitRunner(spawnFn) {
326
+ return {
327
+ run: (gitArguments, cwd) => runBufferedProcess(spawnFn, "git", gitArguments, cwd, false)
328
+ };
329
+ }
330
+ var defaultGitRunner = createGitRunner(spawn);
331
+ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell) {
332
+ return new Promise((resolve) => {
333
+ const proc = spawnFn(command, commandArguments, { cwd, shell });
334
+ const chunks = [];
335
+ proc.stdout?.on("data", (data) => {
336
+ chunks.push(data.toString());
337
+ });
338
+ proc.stderr?.on("data", (data) => {
339
+ chunks.push(data.toString());
340
+ });
341
+ proc.on("close", (code) => {
342
+ resolve({ exitCode: code ?? 1, output: chunks.join("") });
343
+ });
344
+ proc.on("error", (error) => {
345
+ resolve({ exitCode: 1, output: error.message });
346
+ });
347
+ });
348
+ }
293
349
 
294
350
  // lib/cli/commands/lint-markdown.ts
295
351
  import { dirname as dirname2, resolve } from "node:path";
296
352
 
353
+ // lib/artifacts/idea-transition-task.ts
354
+ var IDEA_TRANSITION_PREFIXES = [
355
+ "Refine Idea: ",
356
+ "Create Task From Idea: ",
357
+ "Shelve Idea: "
358
+ ];
359
+ function titleToFilename(title) {
360
+ return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
361
+ }
362
+
297
363
  // lib/markdown/markdown-utilities.ts
298
364
  function extractTitle(content) {
299
365
  const match = content.match(/^#\s+(.+)$/m);
@@ -362,9 +428,6 @@ function validateFilename(filePath) {
362
428
  }
363
429
  return null;
364
430
  }
365
- function titleToFilename(title) {
366
- return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
367
- }
368
431
  function validateTitleFilenameMatch(filePath, content) {
369
432
  const title = extractTitle(content);
370
433
  if (!title) {
@@ -404,6 +467,36 @@ function validateOpeningSentenceLength(filePath, content) {
404
467
  }
405
468
  return null;
406
469
  }
470
+ var NON_IMPERATIVE_STARTERS = new Set([
471
+ "the",
472
+ "a",
473
+ "an",
474
+ "this",
475
+ "that",
476
+ "these",
477
+ "those",
478
+ "we",
479
+ "it",
480
+ "they",
481
+ "you",
482
+ "i"
483
+ ]);
484
+ function validateImperativeOpeningSentence(filePath, content) {
485
+ const openingSentence = extractOpeningSentence(content);
486
+ if (!openingSentence) {
487
+ return null;
488
+ }
489
+ const firstWord = openingSentence.split(/\s/)[0].replace(/[^a-zA-Z]/g, "");
490
+ const lower = firstWord.toLowerCase();
491
+ if (NON_IMPERATIVE_STARTERS.has(lower) || lower.endsWith("ing")) {
492
+ const preview = openingSentence.length > 40 ? `${openingSentence.slice(0, 40)}...` : openingSentence;
493
+ return {
494
+ file: filePath,
495
+ message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
496
+ };
497
+ }
498
+ return null;
499
+ }
407
500
  function validateTaskHeadings(filePath, content) {
408
501
  const violations = [];
409
502
  for (const heading of REQUIRED_HEADINGS) {
@@ -443,6 +536,61 @@ function validateLinks(filePath, content, fileSystem) {
443
536
  }
444
537
  return violations;
445
538
  }
539
+ function validateIdeaOpenQuestions(filePath, content) {
540
+ const violations = [];
541
+ const lines = content.split(`
542
+ `);
543
+ let inOpenQuestions = false;
544
+ let currentQuestionLine = null;
545
+ for (let i = 0;i < lines.length; i++) {
546
+ const line = lines[i];
547
+ if (line.startsWith("## ")) {
548
+ if (inOpenQuestions && currentQuestionLine !== null) {
549
+ violations.push({
550
+ file: filePath,
551
+ message: "Question has no options listed beneath it",
552
+ line: currentQuestionLine
553
+ });
554
+ }
555
+ inOpenQuestions = line === "## Open Questions";
556
+ currentQuestionLine = null;
557
+ continue;
558
+ }
559
+ if (!inOpenQuestions)
560
+ continue;
561
+ if (line.startsWith("### ")) {
562
+ if (currentQuestionLine !== null) {
563
+ violations.push({
564
+ file: filePath,
565
+ message: "Question has no options listed beneath it",
566
+ line: currentQuestionLine
567
+ });
568
+ }
569
+ if (!line.trimEnd().endsWith("?")) {
570
+ violations.push({
571
+ file: filePath,
572
+ message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
573
+ line: i + 1
574
+ });
575
+ currentQuestionLine = null;
576
+ } else {
577
+ currentQuestionLine = i + 1;
578
+ }
579
+ continue;
580
+ }
581
+ if (line.startsWith("#### ")) {
582
+ currentQuestionLine = null;
583
+ }
584
+ }
585
+ if (inOpenQuestions && currentQuestionLine !== null) {
586
+ violations.push({
587
+ file: filePath,
588
+ message: "Question has no options listed beneath it",
589
+ line: currentQuestionLine
590
+ });
591
+ }
592
+ return violations;
593
+ }
446
594
  var SEMANTIC_RULES = [
447
595
  {
448
596
  section: "## Goals",
@@ -506,6 +654,26 @@ function validateSemanticLinks(filePath, content) {
506
654
  }
507
655
  return violations;
508
656
  }
657
+ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
658
+ const title = extractTitle(content);
659
+ if (!title) {
660
+ return null;
661
+ }
662
+ for (const prefix of IDEA_TRANSITION_PREFIXES) {
663
+ if (title.startsWith(prefix)) {
664
+ const ideaTitle = title.slice(prefix.length);
665
+ const ideaFilename = titleToFilename(ideaTitle);
666
+ if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
667
+ return {
668
+ file: filePath,
669
+ message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
670
+ };
671
+ }
672
+ return null;
673
+ }
674
+ }
675
+ return null;
676
+ }
509
677
  function validateGoalHierarchySections(filePath, content) {
510
678
  const violations = [];
511
679
  for (const heading of REQUIRED_GOAL_HEADINGS) {
@@ -735,6 +903,26 @@ async function lintMarkdown(dependencies) {
735
903
  }
736
904
  }
737
905
  }
906
+ const ideasPath = `${dustPath}/ideas`;
907
+ const { files: ideaFiles } = await safeScanDir(glob, ideasPath);
908
+ if (ideaFiles.length > 0) {
909
+ context.stdout("Validating idea files in .dust/ideas/...");
910
+ for (const file of ideaFiles) {
911
+ if (!file.endsWith(".md"))
912
+ continue;
913
+ const filePath = `${ideasPath}/${file}`;
914
+ let content;
915
+ try {
916
+ content = await fileSystem.readFile(filePath);
917
+ } catch (error) {
918
+ if (error.code === "ENOENT") {
919
+ continue;
920
+ }
921
+ throw error;
922
+ }
923
+ violations.push(...validateIdeaOpenQuestions(filePath, content));
924
+ }
925
+ }
738
926
  const tasksPath = `${dustPath}/tasks`;
739
927
  const { files: taskFiles } = await safeScanDir(glob, tasksPath);
740
928
  if (taskFiles.length > 0) {
@@ -758,6 +946,14 @@ async function lintMarkdown(dependencies) {
758
946
  }
759
947
  violations.push(...validateTaskHeadings(filePath, content));
760
948
  violations.push(...validateSemanticLinks(filePath, content));
949
+ const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
950
+ if (imperativeViolation) {
951
+ violations.push(imperativeViolation);
952
+ }
953
+ const ideaTransitionViolation = validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem);
954
+ if (ideaTransitionViolation) {
955
+ violations.push(ideaTransitionViolation);
956
+ }
761
957
  }
762
958
  }
763
959
  const goalsPath = `${dustPath}/goals`;
@@ -800,29 +996,6 @@ async function lintMarkdown(dependencies) {
800
996
  }
801
997
 
802
998
  // lib/cli/commands/check.ts
803
- function createBufferedRunner(spawnFn) {
804
- return {
805
- run: (command, cwd) => {
806
- return new Promise((resolve2) => {
807
- const proc = spawnFn(command, [], { cwd, shell: true });
808
- const chunks = [];
809
- proc.stdout?.on("data", (data) => {
810
- chunks.push(data.toString());
811
- });
812
- proc.stderr?.on("data", (data) => {
813
- chunks.push(data.toString());
814
- });
815
- proc.on("close", (code) => {
816
- resolve2({ exitCode: code ?? 1, output: chunks.join("") });
817
- });
818
- proc.on("error", (error) => {
819
- resolve2({ exitCode: 1, output: error.message });
820
- });
821
- });
822
- }
823
- };
824
- }
825
- var defaultBufferedRunner = createBufferedRunner(spawn);
826
999
  async function runConfiguredChecks(checks, cwd, runner) {
827
1000
  const promises = checks.map(async (check) => {
828
1001
  const startTime = Date.now();
@@ -893,7 +1066,7 @@ function displayResults(results, context) {
893
1066
  context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
894
1067
  return failed.length > 0 ? 1 : 0;
895
1068
  }
896
- async function check(dependencies, bufferedRunner = defaultBufferedRunner) {
1069
+ async function check(dependencies, shellRunner = defaultShellRunner) {
897
1070
  const { context, fileSystem, settings } = dependencies;
898
1071
  if (!settings.checks || settings.checks.length === 0) {
899
1072
  context.stderr("Error: No checks configured in .dust/config/settings.json");
@@ -912,7 +1085,7 @@ async function check(dependencies, bufferedRunner = defaultBufferedRunner) {
912
1085
  if (fileSystem.exists(dustPath)) {
913
1086
  checkPromises.push(runValidationCheck(dependencies));
914
1087
  }
915
- checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, bufferedRunner));
1088
+ checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
916
1089
  const promiseResults = await Promise.all(checkPromises);
917
1090
  const results = [];
918
1091
  for (const result of promiseResults) {
@@ -960,9 +1133,8 @@ var createTemplateCommand = (templateName) => async (dependencies) => {
960
1133
  // lib/cli/commands/implement-task.ts
961
1134
  var implementTask = createTemplateCommand("agent-implement-task");
962
1135
 
963
- // lib/cli/commands/init.ts
964
- var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
965
- var colors = {
1136
+ // lib/cli/colors.ts
1137
+ var ANSI_COLORS = {
966
1138
  reset: "\x1B[0m",
967
1139
  bold: "\x1B[1m",
968
1140
  dim: "\x1B[2m",
@@ -970,6 +1142,32 @@ var colors = {
970
1142
  green: "\x1B[32m",
971
1143
  yellow: "\x1B[33m"
972
1144
  };
1145
+ var NO_COLORS = {
1146
+ reset: "",
1147
+ bold: "",
1148
+ dim: "",
1149
+ cyan: "",
1150
+ green: "",
1151
+ yellow: ""
1152
+ };
1153
+ function shouldDisableColors() {
1154
+ if (process.env.NO_COLOR !== undefined) {
1155
+ return true;
1156
+ }
1157
+ if (process.env.TERM === "dumb") {
1158
+ return true;
1159
+ }
1160
+ if (!process.stdout.isTTY) {
1161
+ return true;
1162
+ }
1163
+ return false;
1164
+ }
1165
+ function getColors() {
1166
+ return shouldDisableColors() ? NO_COLORS : ANSI_COLORS;
1167
+ }
1168
+
1169
+ // lib/cli/commands/init.ts
1170
+ var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
973
1171
  function generateSettings(cwd, fileSystem) {
974
1172
  const dustCommand = detectDustCommand(cwd, fileSystem);
975
1173
  const testCommand = detectTestCommand(cwd, fileSystem);
@@ -985,6 +1183,7 @@ This project uses [dust](https://github.com/joshski/dust) for planning and docum
985
1183
  `;
986
1184
  async function init(dependencies) {
987
1185
  const { context, fileSystem } = dependencies;
1186
+ const colors = getColors();
988
1187
  const dustPath = `${context.cwd}/.dust`;
989
1188
  const dustCommand = detectDustCommand(context.cwd, fileSystem);
990
1189
  const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
@@ -1057,37 +1256,6 @@ async function init(dependencies) {
1057
1256
 
1058
1257
  // lib/cli/commands/list.ts
1059
1258
  import { basename } from "node:path";
1060
-
1061
- // lib/cli/colors.ts
1062
- var ANSI_COLORS = {
1063
- reset: "\x1B[0m",
1064
- bold: "\x1B[1m",
1065
- dim: "\x1B[2m",
1066
- cyan: "\x1B[36m"
1067
- };
1068
- var NO_COLORS = {
1069
- reset: "",
1070
- bold: "",
1071
- dim: "",
1072
- cyan: ""
1073
- };
1074
- function shouldDisableColors() {
1075
- if (process.env.NO_COLOR !== undefined) {
1076
- return true;
1077
- }
1078
- if (process.env.TERM === "dumb") {
1079
- return true;
1080
- }
1081
- if (!process.stdout.isTTY) {
1082
- return true;
1083
- }
1084
- return false;
1085
- }
1086
- function getColors() {
1087
- return shouldDisableColors() ? NO_COLORS : ANSI_COLORS;
1088
- }
1089
-
1090
- // lib/cli/commands/list.ts
1091
1259
  var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
1092
1260
  var SECTION_HEADERS = {
1093
1261
  tasks: "\uD83D\uDCCB Tasks",
@@ -1149,7 +1317,7 @@ function renderHierarchy(nodes, output, prefix = "") {
1149
1317
  async function list(dependencies) {
1150
1318
  const { arguments: commandArguments, context, fileSystem } = dependencies;
1151
1319
  const dustPath = `${context.cwd}/.dust`;
1152
- const colors2 = getColors();
1320
+ const colors = getColors();
1153
1321
  if (!fileSystem.exists(dustPath)) {
1154
1322
  context.stderr("Error: .dust directory not found");
1155
1323
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -1185,7 +1353,7 @@ async function list(dependencies) {
1185
1353
  if (type === "goals") {
1186
1354
  const hierarchy = await buildGoalHierarchy(dirPath, fileSystem);
1187
1355
  if (hierarchy.length > 0) {
1188
- context.stdout(`${colors2.dim}Hierarchy:${colors2.reset}`);
1356
+ context.stdout(`${colors.dim}Hierarchy:${colors.reset}`);
1189
1357
  renderHierarchy(hierarchy, (line) => context.stdout(line));
1190
1358
  context.stdout("");
1191
1359
  }
@@ -1197,14 +1365,14 @@ async function list(dependencies) {
1197
1365
  const openingSentence = extractOpeningSentence(content);
1198
1366
  const relativePath = `.dust/${type}/${file}`;
1199
1367
  if (title) {
1200
- context.stdout(`${colors2.bold}# ${title}${colors2.reset}`);
1368
+ context.stdout(`${colors.bold}# ${title}${colors.reset}`);
1201
1369
  } else {
1202
- context.stdout(`${colors2.bold}# ${file.replace(".md", "")}${colors2.reset}`);
1370
+ context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
1203
1371
  }
1204
1372
  if (openingSentence) {
1205
- context.stdout(`${colors2.dim}${openingSentence}${colors2.reset}`);
1373
+ context.stdout(`${colors.dim}${openingSentence}${colors.reset}`);
1206
1374
  }
1207
- context.stdout(`${colors2.cyan}→ ${relativePath}${colors2.reset}`);
1375
+ context.stdout(`${colors.cyan}→ ${relativePath}${colors.reset}`);
1208
1376
  context.stdout("");
1209
1377
  }
1210
1378
  }
@@ -1629,7 +1797,7 @@ function extractBlockedBy(content) {
1629
1797
  async function next(dependencies) {
1630
1798
  const { context, fileSystem } = dependencies;
1631
1799
  const dustPath = `${context.cwd}/.dust`;
1632
- const colors2 = getColors();
1800
+ const colors = getColors();
1633
1801
  if (!fileSystem.exists(dustPath)) {
1634
1802
  context.stderr("Error: .dust directory not found");
1635
1803
  context.stderr("Run 'dust init' to initialize a Dust repository");
@@ -1666,11 +1834,11 @@ async function next(dependencies) {
1666
1834
  for (const task of unblockedTasks) {
1667
1835
  const parts = task.path.split("/");
1668
1836
  const displayTitle = task.title || parts[parts.length - 1].replace(".md", "");
1669
- context.stdout(`${colors2.bold}# ${displayTitle}${colors2.reset}`);
1837
+ context.stdout(`${colors.bold}# ${displayTitle}${colors.reset}`);
1670
1838
  if (task.openingSentence) {
1671
- context.stdout(`${colors2.dim}${task.openingSentence}${colors2.reset}`);
1839
+ context.stdout(`${colors.dim}${task.openingSentence}${colors.reset}`);
1672
1840
  }
1673
- context.stdout(`${colors2.cyan}→ ${task.path}${colors2.reset}`);
1841
+ context.stdout(`${colors.cyan}→ ${task.path}${colors.reset}`);
1674
1842
  context.stdout("");
1675
1843
  }
1676
1844
  return { exitCode: 0 };
@@ -1693,9 +1861,10 @@ function formatEvent(event) {
1693
1861
  return `\uD83D\uDE34 No tasks available. Sleeping...
1694
1862
  `;
1695
1863
  case "loop.tasks_found":
1696
- return "✨ Found task(s). \uD83E\uDD16 Starting Claude...";
1864
+ return `✨ Found a task. Going to work!
1865
+ `;
1697
1866
  case "claude.started":
1698
- return "\uD83E\uDD16 Claude session started";
1867
+ return "\uD83E\uDD16 Starting Claude...";
1699
1868
  case "claude.ended":
1700
1869
  return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1701
1870
  case "claude.raw_event":
@@ -1879,7 +2048,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
1879
2048
  const iterationOptions = {};
1880
2049
  if (eventsUrl) {
1881
2050
  iterationOptions.onRawEvent = (rawEvent) => {
1882
- if (rawEvent.type === "result" && typeof rawEvent.session_id === "string" && rawEvent.session_id) {
2051
+ if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
1883
2052
  agentSessionId = rawEvent.session_id;
1884
2053
  }
1885
2054
  emit({ type: "claude.raw_event", rawEvent });
@@ -1916,30 +2085,6 @@ var newTask = createTemplateCommand("agent-new-task");
1916
2085
  var pickTask = createTemplateCommand("agent-pick-task");
1917
2086
 
1918
2087
  // lib/cli/commands/pre-push.ts
1919
- import { spawn as spawn2 } from "node:child_process";
1920
- function createGitRunner(spawnFn) {
1921
- return {
1922
- run: (gitArguments, cwd) => {
1923
- return new Promise((resolve2) => {
1924
- const proc = spawnFn("git", gitArguments, { cwd });
1925
- const chunks = [];
1926
- proc.stdout?.on("data", (data) => {
1927
- chunks.push(data.toString());
1928
- });
1929
- proc.stderr?.on("data", (data) => {
1930
- chunks.push(data.toString());
1931
- });
1932
- proc.on("close", (code) => {
1933
- resolve2({ exitCode: code ?? 1, output: chunks.join("") });
1934
- });
1935
- proc.on("error", (error) => {
1936
- resolve2({ exitCode: 1, output: error.message });
1937
- });
1938
- });
1939
- }
1940
- };
1941
- }
1942
- var defaultGitRunner = createGitRunner(spawn2);
1943
2088
  function parseGitDiffNameStatus(output) {
1944
2089
  const changes = [];
1945
2090
  const lines = output.trim().split(`
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dust": "./dist/dust.js"
8
8
  },
9
+ "exports": {
10
+ "./artifacts": "./dist/artifacts.js"
11
+ },
9
12
  "files": [
10
13
  "dist",
11
14
  "bin",
@@ -24,7 +27,7 @@
24
27
  "author": "joshski",
25
28
  "license": "MIT",
26
29
  "scripts": {
27
- "build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && cp -r lib/templates templates",
30
+ "build": "bun build lib/cli/run.ts --target node --outfile dist/dust.js && printf '%s\\n%s' '#!/usr/bin/env node' \"$(cat dist/dust.js)\" > dist/dust.js && bun build lib/artifacts/idea-transition-task.ts --target browser --outfile dist/artifacts.js && cp -r lib/templates templates",
28
31
  "test": "vitest run",
29
32
  "test:coverage": "vitest run --coverage",
30
33
  "eval": "bun run ./evals/run.ts"
@@ -1,4 +1,10 @@
1
1
  🤖 Hello {{agentName}}, welcome to dust!
2
+ {{#if agentInstructions}}
3
+
4
+ ## Project Instructions
5
+
6
+ {{agentInstructions}}
7
+ {{/if}}
2
8
 
3
9
  CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
4
10
 
@@ -6,6 +6,41 @@ Follow these steps:
6
6
  2. Create a new markdown file in `.dust/ideas/` with a descriptive kebab-case name (e.g., `improve-error-messages.md`)
7
7
  3. Add a title as the first line using an H1 heading (e.g., `# Improve error messages`)
8
8
  4. Write a brief description of the potential change or improvement
9
- 5. Run `{{bin}} lint markdown` to catch any issues with the idea file format
10
- 6. Create a single atomic commit with a message in the format "Add idea: <title>"
11
- 7. Push your commit to the remote repository
9
+ 5. If the idea has open questions, add an `## Open Questions` section (see below)
10
+ 6. Run `{{bin}} lint markdown` to catch any issues with the idea file format
11
+ 7. Create a single atomic commit with a message in the format "Add idea: <title>"
12
+ 8. Push your commit to the remote repository
13
+
14
+ ### Open Questions section
15
+
16
+ Ideas exist to eventually spawn tasks, so they start intentionally vague. An optional `## Open Questions` section captures the decisions that need to be made before the idea becomes actionable. Each question is an h3 heading ending with `?`, and each option is an h4 heading with markdown content explaining the trade-offs:
17
+
18
+ ```markdown
19
+ ## Open Questions
20
+
21
+ ### Should we take our own payments?
22
+
23
+ #### Yes, take our own payments
24
+
25
+ Lower costs and we become the seller of record, but requires a merchant account.
26
+
27
+ #### No, use a payment provider
28
+
29
+ Higher costs but simpler setup. No merchant account needed.
30
+
31
+ ### Which storage backend should we use?
32
+
33
+ #### SQLite
34
+
35
+ Simple and embedded. Good for single-node deployments.
36
+
37
+ #### PostgreSQL
38
+
39
+ Scalable but requires a separate server.
40
+ ```
41
+
42
+ Rules:
43
+ - Questions are `###` headings and must end with `?`
44
+ - Options are `####` headings beneath a question
45
+ - Each question must have at least one option
46
+ - Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
@@ -17,7 +17,7 @@ Use a todo list to track your progress through these steps.
17
17
  - The goal is a task description with minimal ambiguity at implementation time
18
18
  4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)
19
19
  5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)
20
- 6. Write a comprehensive description of what needs to be done with technical details and references to relevant files
20
+ 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.
21
21
  7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)
22
22
  8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers
23
23
  9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item
@@ -1,4 +1,4 @@
1
- 💨 dust - A tool for keeping AI coding agents on track.
1
+ 💨 dust - Flow state for AI coding agents.
2
2
 
3
3
  Usage: {{bin}} <command> [options]
4
4
 
@@ -1,4 +1,10 @@
1
1
  🤖 Hello {{agentName}}, welcome to dust!
2
+ {{#if agentInstructions}}
3
+
4
+ ## Project Instructions
5
+
6
+ {{agentInstructions}}
7
+ {{/if}}
2
8
 
3
9
  CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
4
10
 
@@ -6,6 +6,41 @@ Follow these steps:
6
6
  2. Create a new markdown file in `.dust/ideas/` with a descriptive kebab-case name (e.g., `improve-error-messages.md`)
7
7
  3. Add a title as the first line using an H1 heading (e.g., `# Improve error messages`)
8
8
  4. Write a brief description of the potential change or improvement
9
- 5. Run `{{bin}} lint markdown` to catch any issues with the idea file format
10
- 6. Create a single atomic commit with a message in the format "Add idea: <title>"
11
- 7. Push your commit to the remote repository
9
+ 5. If the idea has open questions, add an `## Open Questions` section (see below)
10
+ 6. Run `{{bin}} lint markdown` to catch any issues with the idea file format
11
+ 7. Create a single atomic commit with a message in the format "Add idea: <title>"
12
+ 8. Push your commit to the remote repository
13
+
14
+ ### Open Questions section
15
+
16
+ Ideas exist to eventually spawn tasks, so they start intentionally vague. An optional `## Open Questions` section captures the decisions that need to be made before the idea becomes actionable. Each question is an h3 heading ending with `?`, and each option is an h4 heading with markdown content explaining the trade-offs:
17
+
18
+ ```markdown
19
+ ## Open Questions
20
+
21
+ ### Should we take our own payments?
22
+
23
+ #### Yes, take our own payments
24
+
25
+ Lower costs and we become the seller of record, but requires a merchant account.
26
+
27
+ #### No, use a payment provider
28
+
29
+ Higher costs but simpler setup. No merchant account needed.
30
+
31
+ ### Which storage backend should we use?
32
+
33
+ #### SQLite
34
+
35
+ Simple and embedded. Good for single-node deployments.
36
+
37
+ #### PostgreSQL
38
+
39
+ Scalable but requires a separate server.
40
+ ```
41
+
42
+ Rules:
43
+ - Questions are `###` headings and must end with `?`
44
+ - Options are `####` headings beneath a question
45
+ - Each question must have at least one option
46
+ - Options can contain any markdown content (paragraphs, lists, code blocks, etc.)
@@ -17,7 +17,7 @@ Use a todo list to track your progress through these steps.
17
17
  - The goal is a task description with minimal ambiguity at implementation time
18
18
  4. Create a new markdown file in `.dust/tasks/` with a descriptive kebab-case name (e.g., `add-user-authentication.md`)
19
19
  5. Add a title as the first line using an H1 heading (e.g., `# Add user authentication`)
20
- 6. Write a comprehensive description of what needs to be done with technical details and references to relevant files
20
+ 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.
21
21
  7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)
22
22
  8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers
23
23
  9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item
@@ -1,4 +1,4 @@
1
- 💨 dust - A tool for keeping AI coding agents on track.
1
+ 💨 dust - Flow state for AI coding agents.
2
2
 
3
3
  Usage: {{bin}} <command> [options]
4
4