@joshski/dust 0.1.36 → 0.1.38

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
@@ -693,22 +693,20 @@ function getLogLines(buffer) {
693
693
  import { join as join7 } from "node:path";
694
694
 
695
695
  // lib/agent-events.ts
696
- function mapToAgentEvent(event) {
696
+ function rawEventToAgentEvent(rawEvent) {
697
+ if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
698
+ return { type: "agent-session-activity" };
699
+ }
700
+ return { type: "claude-event", rawEvent };
701
+ }
702
+ function formatAgentEvent(event) {
697
703
  switch (event.type) {
698
- case "claude.started":
699
- return { type: "agent-session-started" };
700
- case "claude.ended":
701
- return {
702
- type: "agent-session-ended",
703
- success: event.success,
704
- error: event.error
705
- };
706
- case "claude.raw_event":
707
- if (typeof event.rawEvent.type === "string" && event.rawEvent.type === "stream_event") {
708
- return { type: "agent-session-activity" };
709
- }
710
- return { type: "claude-event", rawEvent: event.rawEvent };
711
- default:
704
+ case "agent-session-started":
705
+ return `\uD83E\uDD16 Starting Claude: ${event.title}`;
706
+ case "agent-session-ended":
707
+ return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
708
+ case "agent-session-activity":
709
+ case "claude-event":
712
710
  return null;
713
711
  }
714
712
  }
@@ -1115,6 +1113,42 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1115
1113
  // lib/cli/commands/loop.ts
1116
1114
  import { spawn as nodeSpawn2 } from "node:child_process";
1117
1115
 
1116
+ // lib/cli/commands/focus.ts
1117
+ function buildImplementationInstructions(bin, hooksInstalled) {
1118
+ const steps = [];
1119
+ let step = 1;
1120
+ steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
1121
+ step++;
1122
+ steps.push(`${step}. Implement the task`);
1123
+ step++;
1124
+ if (!hooksInstalled) {
1125
+ steps.push(`${step}. Run \`${bin} check\` before committing`);
1126
+ step++;
1127
+ }
1128
+ steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
1129
+ step++;
1130
+ steps.push(`${step}. Push your commit to the remote repository`);
1131
+ steps.push("");
1132
+ steps.push("Keep your change small and focused. One task, one commit.");
1133
+ return steps.join(`
1134
+ `);
1135
+ }
1136
+ async function focus(dependencies) {
1137
+ const { context, settings } = dependencies;
1138
+ const objective = dependencies.arguments.join(" ").trim();
1139
+ if (!objective) {
1140
+ context.stderr("Error: No objective provided");
1141
+ context.stderr('Usage: dust focus "your objective here"');
1142
+ return { exitCode: 1 };
1143
+ }
1144
+ const hooksInstalled = await manageGitHooks(dependencies);
1145
+ const vars = templateVariables(settings, hooksInstalled);
1146
+ context.stdout(`\uD83C\uDFAF Focus: ${objective}`);
1147
+ context.stdout("");
1148
+ context.stdout(buildImplementationInstructions(vars.bin, hooksInstalled));
1149
+ return { exitCode: 0 };
1150
+ }
1151
+
1118
1152
  // lib/cli/commands/next.ts
1119
1153
  function extractBlockedBy(content) {
1120
1154
  const blockedByMatch = content.match(/^## Blocked By\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
@@ -1195,7 +1229,7 @@ async function next(dependencies) {
1195
1229
  }
1196
1230
 
1197
1231
  // lib/cli/commands/loop.ts
1198
- function formatEvent(event) {
1232
+ function formatLoopEvent(event) {
1199
1233
  switch (event.type) {
1200
1234
  case "loop.warning":
1201
1235
  return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
@@ -1213,16 +1247,12 @@ function formatEvent(event) {
1213
1247
  case "loop.tasks_found":
1214
1248
  return `✨ Found a task. Going to work!
1215
1249
  `;
1216
- case "claude.started":
1217
- return "\uD83E\uDD16 Starting Claude...";
1218
- case "claude.ended":
1219
- return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1220
- case "claude.raw_event":
1221
- return null;
1222
1250
  case "loop.iteration_complete":
1223
1251
  return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
1224
1252
  case "loop.ended":
1225
1253
  return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
1254
+ case "loop.start_agent":
1255
+ return null;
1226
1256
  }
1227
1257
  }
1228
1258
  async function defaultPostEvent(url, payload) {
@@ -1240,21 +1270,18 @@ function createDefaultDependencies() {
1240
1270
  postEvent: defaultPostEvent
1241
1271
  };
1242
1272
  }
1243
- function createEventPoster(eventsUrl, sessionId, postEvent, onError, getAgentSessionId, repository = "") {
1273
+ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgentSessionId, repository = "") {
1244
1274
  let sequence = 0;
1245
1275
  return (event) => {
1246
1276
  if (!eventsUrl)
1247
1277
  return;
1248
- const agentEvent = mapToAgentEvent(event);
1249
- if (!agentEvent)
1250
- return;
1251
1278
  sequence++;
1252
1279
  const payload = {
1253
1280
  sequence,
1254
1281
  timestamp: new Date().toISOString(),
1255
1282
  sessionId,
1256
1283
  repository,
1257
- event: agentEvent
1284
+ event
1258
1285
  };
1259
1286
  const agentSessionId = getAgentSessionId?.();
1260
1287
  if (agentSessionId) {
@@ -1287,29 +1314,26 @@ async function gitPull(cwd, spawn) {
1287
1314
  });
1288
1315
  });
1289
1316
  }
1290
- async function hasAvailableTasks(dependencies) {
1291
- let hasOutput = false;
1292
- const captureContext = {
1293
- ...dependencies.context,
1294
- stdout: () => {
1295
- hasOutput = true;
1296
- }
1297
- };
1298
- await next({ ...dependencies, context: captureContext });
1299
- return hasOutput;
1317
+ async function findAvailableTasks(dependencies) {
1318
+ const { context, fileSystem } = dependencies;
1319
+ const result = await findUnblockedTasks(context.cwd, fileSystem);
1320
+ return result.tasks;
1300
1321
  }
1301
- async function runOneIteration(dependencies, loopDependencies, emit, options = {}) {
1322
+ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, options = {}) {
1302
1323
  const { context } = dependencies;
1303
1324
  const { spawn, run: run2 } = loopDependencies;
1304
1325
  const { onRawEvent } = options;
1305
- emit({ type: "loop.syncing" });
1326
+ onLoopEvent({ type: "loop.syncing" });
1306
1327
  const pullResult = await gitPull(context.cwd, spawn);
1307
1328
  if (!pullResult.success) {
1308
- emit({
1329
+ onLoopEvent({
1309
1330
  type: "loop.sync_skipped",
1310
- reason: pullResult.message ?? "unknown error"
1331
+ reason: pullResult.message
1332
+ });
1333
+ onAgentEvent?.({
1334
+ type: "agent-session-started",
1335
+ title: "Resolving git conflict"
1311
1336
  });
1312
- emit({ type: "claude.started" });
1313
1337
  const prompt2 = `git pull failed with the following error:
1314
1338
 
1315
1339
  ${pullResult.message}
@@ -1320,6 +1344,7 @@ Please resolve this issue. Common approaches:
1320
1344
  3. After resolving, commit any changes and push to remote
1321
1345
 
1322
1346
  Make sure the repository is in a clean state and synced with remote before finishing.`;
1347
+ onLoopEvent({ type: "loop.start_agent", prompt: prompt2 });
1323
1348
  try {
1324
1349
  await run2(prompt2, {
1325
1350
  spawnOptions: {
@@ -1329,24 +1354,47 @@ Make sure the repository is in a clean state and synced with remote before finis
1329
1354
  },
1330
1355
  onRawEvent
1331
1356
  });
1332
- emit({ type: "claude.ended", success: true });
1357
+ onAgentEvent?.({ type: "agent-session-ended", success: true });
1333
1358
  return "resolved_pull_conflict";
1334
1359
  } catch (error) {
1335
1360
  const errorMessage = error instanceof Error ? error.message : String(error);
1336
1361
  context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
1337
- emit({ type: "claude.ended", success: false, error: errorMessage });
1362
+ onAgentEvent?.({
1363
+ type: "agent-session-ended",
1364
+ success: false,
1365
+ error: errorMessage
1366
+ });
1338
1367
  }
1339
1368
  }
1340
- emit({ type: "loop.checking_tasks" });
1341
- const hasTasks = await hasAvailableTasks(dependencies);
1342
- if (!hasTasks) {
1343
- emit({ type: "loop.no_tasks" });
1369
+ onLoopEvent({ type: "loop.checking_tasks" });
1370
+ const tasks = await findAvailableTasks(dependencies);
1371
+ if (tasks.length === 0) {
1372
+ onLoopEvent({ type: "loop.no_tasks" });
1344
1373
  return "no_tasks";
1345
1374
  }
1346
- emit({ type: "loop.tasks_found" });
1347
- emit({ type: "claude.started" });
1375
+ const task = tasks[0];
1376
+ onLoopEvent({ type: "loop.tasks_found" });
1377
+ onAgentEvent?.({
1378
+ type: "agent-session-started",
1379
+ title: task.title ?? task.path
1380
+ });
1381
+ const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
1348
1382
  const { dustCommand, installCommand = "npm install" } = dependencies.settings;
1349
- const prompt = `Run \`${installCommand} && ${dustCommand} agent && ${dustCommand} pick task\` and follow the instructions.`;
1383
+ const instructions = buildImplementationInstructions(dustCommand, true);
1384
+ const prompt = `Run \`${installCommand}\` to install dependencies, then implement the following task.
1385
+
1386
+ ## Task: ${task.title}
1387
+
1388
+ The following is the contents of the task file \`${task.path}\`:
1389
+
1390
+ ${taskContent}
1391
+
1392
+ When the task is complete, delete the task file \`${task.path}\`.
1393
+
1394
+ ## Instructions
1395
+
1396
+ ${instructions}`;
1397
+ onLoopEvent({ type: "loop.start_agent", prompt });
1350
1398
  try {
1351
1399
  await run2(prompt, {
1352
1400
  spawnOptions: {
@@ -1356,12 +1404,16 @@ Make sure the repository is in a clean state and synced with remote before finis
1356
1404
  },
1357
1405
  onRawEvent
1358
1406
  });
1359
- emit({ type: "claude.ended", success: true });
1407
+ onAgentEvent?.({ type: "agent-session-ended", success: true });
1360
1408
  return "ran_claude";
1361
1409
  } catch (error) {
1362
1410
  const errorMessage = error instanceof Error ? error.message : String(error);
1363
1411
  context.stderr(`Claude exited with error: ${errorMessage}`);
1364
- emit({ type: "claude.ended", success: false, error: errorMessage });
1412
+ onAgentEvent?.({
1413
+ type: "agent-session-ended",
1414
+ success: false,
1415
+ error: errorMessage
1416
+ });
1365
1417
  return "claude_error";
1366
1418
  }
1367
1419
  }
@@ -1382,46 +1434,49 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
1382
1434
  const eventsUrl = settings.eventsUrl;
1383
1435
  const sessionId = crypto.randomUUID();
1384
1436
  let agentSessionId;
1385
- const postEventFn = createEventPoster(eventsUrl, sessionId, postEvent, (error) => {
1437
+ const sendWireEvent = createWireEventSender(eventsUrl, sessionId, postEvent, (error) => {
1386
1438
  const message = error instanceof Error ? error.message : String(error);
1387
1439
  context.stderr(`Event POST failed: ${message}`);
1388
1440
  }, () => agentSessionId);
1389
- const emit = (event) => {
1390
- const formatted = formatEvent(event);
1441
+ const onLoopEvent = (event) => {
1442
+ const formatted = formatLoopEvent(event);
1391
1443
  if (formatted !== null) {
1392
1444
  context.stdout(formatted);
1393
1445
  }
1394
- postEventFn(event);
1395
1446
  };
1396
- emit({ type: "loop.warning" });
1397
- emit({ type: "loop.started", maxIterations });
1447
+ const onAgentEvent = (event) => {
1448
+ const formatted = formatAgentEvent(event);
1449
+ if (formatted !== null) {
1450
+ context.stdout(formatted);
1451
+ }
1452
+ sendWireEvent(event);
1453
+ };
1454
+ onLoopEvent({ type: "loop.warning" });
1455
+ onLoopEvent({ type: "loop.started", maxIterations });
1398
1456
  context.stdout(" Press Ctrl+C to stop");
1399
1457
  context.stdout("");
1400
1458
  let completedIterations = 0;
1401
1459
  const iterationOptions = {};
1402
1460
  if (eventsUrl) {
1403
1461
  iterationOptions.onRawEvent = (rawEvent) => {
1404
- if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
1405
- agentSessionId = rawEvent.session_id;
1406
- }
1407
- emit({ type: "claude.raw_event", rawEvent });
1462
+ onAgentEvent(rawEventToAgentEvent(rawEvent));
1408
1463
  };
1409
1464
  }
1410
1465
  while (completedIterations < maxIterations) {
1411
- agentSessionId = undefined;
1412
- const result = await runOneIteration(dependencies, loopDependencies, emit, iterationOptions);
1466
+ agentSessionId = crypto.randomUUID();
1467
+ const result = await runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, iterationOptions);
1413
1468
  if (result === "no_tasks") {
1414
1469
  await loopDependencies.sleep(SLEEP_INTERVAL_MS);
1415
1470
  } else {
1416
1471
  completedIterations++;
1417
- emit({
1472
+ onLoopEvent({
1418
1473
  type: "loop.iteration_complete",
1419
1474
  iteration: completedIterations,
1420
1475
  maxIterations
1421
1476
  });
1422
1477
  }
1423
1478
  }
1424
- emit({ type: "loop.ended", maxIterations });
1479
+ onLoopEvent({ type: "loop.ended", maxIterations });
1425
1480
  return { exitCode: 0 };
1426
1481
  }
1427
1482
 
@@ -1534,20 +1589,30 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
1534
1589
  };
1535
1590
  let agentSessionId;
1536
1591
  let sequence = 0;
1537
- const loopEmit = (event) => {
1538
- const formatted = formatEvent(event);
1592
+ const onLoopEvent = (event) => {
1593
+ const formatted = formatLoopEvent(event);
1539
1594
  if (formatted !== null) {
1540
1595
  appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
1541
1596
  }
1542
- const agentEvent = mapToAgentEvent(event);
1543
- if (agentEvent && sendEvent && sessionId) {
1597
+ };
1598
+ const onAgentEvent = (event) => {
1599
+ if (event.type === "agent-session-started") {
1600
+ repoState.agentStatus = "busy";
1601
+ } else if (event.type === "agent-session-ended") {
1602
+ repoState.agentStatus = "idle";
1603
+ }
1604
+ const formatted = formatAgentEvent(event);
1605
+ if (formatted !== null) {
1606
+ appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
1607
+ }
1608
+ if (sendEvent && sessionId) {
1544
1609
  sequence++;
1545
1610
  const msg = {
1546
1611
  sequence,
1547
1612
  timestamp: new Date().toISOString(),
1548
1613
  sessionId,
1549
1614
  repository: repoName,
1550
- event: agentEvent
1615
+ event
1551
1616
  };
1552
1617
  if (agentSessionId) {
1553
1618
  msg.agentSessionId = agentSessionId;
@@ -1556,13 +1621,10 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
1556
1621
  }
1557
1622
  };
1558
1623
  while (!repoState.stopRequested) {
1559
- agentSessionId = undefined;
1560
- const result = await runOneIteration(commandDeps, loopDeps, loopEmit, {
1624
+ agentSessionId = crypto.randomUUID();
1625
+ const result = await runOneIteration(commandDeps, loopDeps, onLoopEvent, onAgentEvent, {
1561
1626
  onRawEvent: (rawEvent) => {
1562
- if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
1563
- agentSessionId = rawEvent.session_id;
1564
- }
1565
- loopEmit({ type: "claude.raw_event", rawEvent });
1627
+ onAgentEvent(rawEventToAgentEvent(rawEvent));
1566
1628
  }
1567
1629
  });
1568
1630
  if (result === "no_tasks") {
@@ -1596,7 +1658,8 @@ async function addRepository(repository, manager, repoDeps, context) {
1596
1658
  path: repoPath,
1597
1659
  loopPromise: null,
1598
1660
  stopRequested: false,
1599
- logBuffer: manager.logBuffers.get(repository.name) ?? createLogBuffer()
1661
+ logBuffer: manager.logBuffers.get(repository.name) ?? createLogBuffer(),
1662
+ agentStatus: "idle"
1600
1663
  };
1601
1664
  manager.repositories.set(repository.name, repoState);
1602
1665
  const addedEvent = {
@@ -1726,6 +1789,7 @@ function createTerminalUIState() {
1726
1789
  repositories: [],
1727
1790
  selectedIndex: -1,
1728
1791
  logBuffers: new Map,
1792
+ agentStatuses: new Map,
1729
1793
  scrollOffset: 0,
1730
1794
  autoScroll: true,
1731
1795
  width: 80,
@@ -1747,6 +1811,7 @@ function addRepository2(state, name, logBuffer) {
1747
1811
  return -1;
1748
1812
  return a.localeCompare(b);
1749
1813
  });
1814
+ state.agentStatuses.set(name, "idle");
1750
1815
  }
1751
1816
  state.logBuffers.set(name, logBuffer);
1752
1817
  }
@@ -1755,6 +1820,7 @@ function removeRepository2(state, name) {
1755
1820
  if (index >= 0) {
1756
1821
  state.repositories.splice(index, 1);
1757
1822
  state.logBuffers.delete(name);
1823
+ state.agentStatuses.delete(name);
1758
1824
  if (state.selectedIndex >= state.repositories.length) {
1759
1825
  state.selectedIndex = state.repositories.length - 1;
1760
1826
  }
@@ -1805,7 +1871,7 @@ function getTabRowCount(state) {
1805
1871
  return 1;
1806
1872
  const tabWidths = [5];
1807
1873
  for (const name of state.repositories) {
1808
- tabWidths.push(name.length + 2);
1874
+ tabWidths.push(name.length + 4);
1809
1875
  }
1810
1876
  let rows = 1;
1811
1877
  let currentRowWidth = 0;
@@ -1873,14 +1939,17 @@ function renderTabs(state) {
1873
1939
  for (let i = 0;i < state.repositories.length; i++) {
1874
1940
  const name = state.repositories[i];
1875
1941
  const color = getRepoColor(name, i);
1876
- const width = name.length + 2;
1942
+ const agentStatus = state.agentStatuses.get(name) ?? "idle";
1943
+ const dotColor = agentStatus === "busy" ? ANSI.FG_GREEN : ANSI.DIM;
1944
+ const dot = `${dotColor}●${ANSI.RESET}`;
1945
+ const width = name.length + 4;
1877
1946
  if (i === state.selectedIndex) {
1878
1947
  tabs.push({
1879
- text: `${ANSI.INVERSE}${color} ${name} ${ANSI.RESET}`,
1948
+ text: `${ANSI.INVERSE} ${dot}${ANSI.INVERSE}${color} ${name} ${ANSI.RESET}`,
1880
1949
  width
1881
1950
  });
1882
1951
  } else {
1883
- tabs.push({ text: `${color} ${name} ${ANSI.RESET}`, width });
1952
+ tabs.push({ text: ` ${dot}${color} ${name} ${ANSI.RESET}`, width });
1884
1953
  }
1885
1954
  }
1886
1955
  const rows = [[]];
@@ -2207,12 +2276,18 @@ function syncUIWithRepoList(state, repos) {
2207
2276
  }
2208
2277
  }
2209
2278
  }
2279
+ function syncAgentStatuses(state) {
2280
+ for (const [name, repoState] of state.repositories) {
2281
+ state.ui.agentStatuses.set(name, repoState.agentStatus);
2282
+ }
2283
+ }
2210
2284
  function syncTUI(state) {
2211
2285
  const currentUIRepos = new Set(state.ui.repositories);
2212
2286
  const currentRepos = new Set(state.repositories.keys());
2213
2287
  for (const [name, repoState] of state.repositories) {
2214
2288
  state.logBuffers.set(name, repoState.logBuffer);
2215
2289
  addRepository2(state.ui, name, repoState.logBuffer);
2290
+ state.ui.agentStatuses.set(name, repoState.agentStatus);
2216
2291
  }
2217
2292
  for (const name of currentUIRepos) {
2218
2293
  if (name !== "system" && !currentRepos.has(name)) {
@@ -2342,6 +2417,7 @@ function setupTUI(state, bucketDeps) {
2342
2417
  });
2343
2418
  const renderInterval = setInterval(() => {
2344
2419
  if (!state.shuttingDown) {
2420
+ syncAgentStatuses(state);
2345
2421
  bucketDeps.writeStdout(renderFrame(state.ui));
2346
2422
  }
2347
2423
  }, 100);
@@ -3260,39 +3336,6 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
3260
3336
  return { exitCode };
3261
3337
  }
3262
3338
 
3263
- // lib/cli/commands/focus.ts
3264
- async function focus(dependencies) {
3265
- const { context, settings } = dependencies;
3266
- const objective = dependencies.arguments.join(" ").trim();
3267
- if (!objective) {
3268
- context.stderr("Error: No objective provided");
3269
- context.stderr('Usage: dust focus "your objective here"');
3270
- return { exitCode: 1 };
3271
- }
3272
- const hooksInstalled = await manageGitHooks(dependencies);
3273
- const vars = templateVariables(settings, hooksInstalled);
3274
- context.stdout(`\uD83C\uDFAF Focus: ${objective}`);
3275
- context.stdout("");
3276
- const steps = [];
3277
- let step = 1;
3278
- steps.push(`${step}. Run \`${vars.bin} check\` to verify the project is in a good state`);
3279
- step++;
3280
- steps.push(`${step}. Implement the task`);
3281
- step++;
3282
- if (!hooksInstalled) {
3283
- steps.push(`${step}. Run \`${vars.bin} check\` before committing`);
3284
- step++;
3285
- }
3286
- steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
3287
- step++;
3288
- steps.push(`${step}. Push your commit to the remote repository`);
3289
- steps.push("");
3290
- steps.push("Keep your change small and focused. One task, one commit.");
3291
- context.stdout(steps.join(`
3292
- `));
3293
- return { exitCode: 0 };
3294
- }
3295
-
3296
3339
  // lib/cli/commands/help.ts
3297
3340
  function generateHelpText(settings) {
3298
3341
  return loadTemplate("help", { bin: settings.dustCommand });
@@ -1,6 +1,7 @@
1
1
  import type { FileSystem } from './cli/types';
2
2
  export declare const IDEA_TRANSITION_PREFIXES: string[];
3
3
  export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
4
+ export declare const BUILD_IDEA_PREFIX = "Build Idea: ";
4
5
  export interface IdeaInProgress {
5
6
  taskSlug: string;
6
7
  ideaTitle: string;
@@ -8,6 +9,7 @@ export interface IdeaInProgress {
8
9
  export interface ParsedCaptureIdeaTask {
9
10
  ideaTitle: string;
10
11
  ideaDescription: string;
12
+ buildItNow: boolean;
11
13
  }
12
14
  export declare function findAllCaptureIdeaTasks(fileSystem: FileSystem, dustPath: string): Promise<IdeaInProgress[]>;
13
15
  /**
@@ -42,5 +44,9 @@ export interface DecomposeIdeaOptions {
42
44
  export declare function createRefineIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
43
45
  export declare function decomposeIdea(fileSystem: FileSystem, dustPath: string, options: DecomposeIdeaOptions): Promise<CreateIdeaTransitionTaskResult>;
44
46
  export declare function createShelveIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
45
- export declare function createCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, title: string, description: string): Promise<CreateIdeaTransitionTaskResult>;
47
+ export declare function createCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, options: {
48
+ title: string;
49
+ description: string;
50
+ buildItNow?: boolean;
51
+ }): Promise<CreateIdeaTransitionTaskResult>;
46
52
  export declare function parseCaptureIdeaTask(fileSystem: FileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
@@ -5,6 +5,7 @@ var IDEA_TRANSITION_PREFIXES = [
5
5
  "Shelve Idea: "
6
6
  ];
7
7
  var CAPTURE_IDEA_PREFIX = "Add Idea: ";
8
+ var BUILD_IDEA_PREFIX = "Build Idea: ";
8
9
  async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
9
10
  const tasksPath = `${dustPath}/tasks`;
10
11
  if (!fileSystem.exists(tasksPath))
@@ -22,6 +23,11 @@ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
22
23
  taskSlug: file.replace(/\.md$/, ""),
23
24
  ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
24
25
  });
26
+ } else if (title.startsWith(BUILD_IDEA_PREFIX)) {
27
+ results.push({
28
+ taskSlug: file.replace(/\.md$/, ""),
29
+ ideaTitle: title.slice(BUILD_IDEA_PREFIX.length)
30
+ });
25
31
  }
26
32
  }
27
33
  return results;
@@ -112,7 +118,7 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
112
118
  ], { description });
113
119
  }
114
120
  async function decomposeIdea(fileSystem, dustPath, options) {
115
- return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks -- split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/goals/\` to link relevant goals and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
121
+ return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/goals/\` to link relevant goals and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
116
122
  "One or more new tasks are created in .dust/tasks/",
117
123
  "Task's Goals section links to relevant goals from .dust/goals/",
118
124
  "The original idea is deleted or updated to reflect remaining scope"
@@ -124,14 +130,44 @@ async function decomposeIdea(fileSystem, dustPath, options) {
124
130
  async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
125
131
  return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], { description });
126
132
  }
127
- async function createCaptureIdeaTask(fileSystem, dustPath, title, description) {
133
+ async function createCaptureIdeaTask(fileSystem, dustPath, options) {
134
+ const { title, description, buildItNow } = options;
128
135
  if (!title || !title.trim()) {
129
136
  throw new Error("title is required and must not be whitespace-only");
130
137
  }
131
138
  if (!description || !description.trim()) {
132
139
  throw new Error("description is required and must not be whitespace-only");
133
140
  }
134
- const taskTitle = `Add Idea: ${title}`;
141
+ if (buildItNow) {
142
+ const taskTitle2 = `${BUILD_IDEA_PREFIX}${title}`;
143
+ const filename2 = titleToFilename(taskTitle2);
144
+ const filePath2 = `${dustPath}/tasks/${filename2}`;
145
+ const content2 = `# ${taskTitle2}
146
+
147
+ Research this idea thoroughly, review \`.dust/goals/\` and \`.dust/facts/\` for relevant context, then create one or more narrowly-scoped task files in \`.dust/tasks/\`. Each task should deliver a thin but complete vertical slice of working software.
148
+
149
+ ## Idea Description
150
+
151
+ ${description}
152
+
153
+ ## Goals
154
+
155
+ (none)
156
+
157
+ ## Blocked By
158
+
159
+ (none)
160
+
161
+ ## Definition of Done
162
+
163
+ - [ ] One or more new tasks are created in \`.dust/tasks/\`
164
+ - [ ] Tasks link to relevant goals from \`.dust/goals/\`
165
+ - [ ] Tasks are narrowly scoped vertical slices
166
+ `;
167
+ await fileSystem.writeFile(filePath2, content2);
168
+ return { filePath: filePath2 };
169
+ }
170
+ const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
135
171
  const filename = titleToFilename(taskTitle);
136
172
  const filePath = `${dustPath}/tasks/${filename}`;
137
173
  const ideaFilename = titleToFilename(title);
@@ -173,16 +209,23 @@ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
173
209
  return null;
174
210
  }
175
211
  const title = titleMatch[1].trim();
176
- if (!title.startsWith(CAPTURE_IDEA_PREFIX)) {
212
+ let ideaTitle;
213
+ let buildItNow;
214
+ if (title.startsWith(BUILD_IDEA_PREFIX)) {
215
+ ideaTitle = title.slice(BUILD_IDEA_PREFIX.length);
216
+ buildItNow = true;
217
+ } else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
218
+ ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
219
+ buildItNow = false;
220
+ } else {
177
221
  return null;
178
222
  }
179
- const ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
180
223
  const descriptionMatch = content.match(/^## Idea Description\n\n([\s\S]*?)\n\n## /m);
181
224
  if (!descriptionMatch) {
182
225
  return null;
183
226
  }
184
227
  const ideaDescription = descriptionMatch[1];
185
- return { ideaTitle, ideaDescription };
228
+ return { ideaTitle, ideaDescription, buildItNow };
186
229
  }
187
230
  export {
188
231
  titleToFilename,
@@ -194,5 +237,6 @@ export {
194
237
  createRefineIdeaTask,
195
238
  createCaptureIdeaTask,
196
239
  IDEA_TRANSITION_PREFIXES,
197
- CAPTURE_IDEA_PREFIX
240
+ CAPTURE_IDEA_PREFIX,
241
+ BUILD_IDEA_PREFIX
198
242
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@ Use a todo list to track your progress through these steps.
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
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
+ - Scope the task as a **vertical slice**: a thin but complete path through the system that can be tested and built upon, rather than an isolated component or layer.
21
22
  7. Add a `## Goals` section with links to relevant goals this task supports (e.g., `- [Goal Name](../goals/goal-name.md)`)
22
23
  8. Add a `## Blocked By` section listing any tasks that must complete first, or `(none)` if there are no blockers
23
24
  9. Add a `## Definition of Done` section with a checklist of completion criteria using `- [ ]` for each item
@@ -1,31 +0,0 @@
1
- 🤖 Hello {{agentName}}, welcome to dust!
2
-
3
- CRITICAL: You MUST run exactly ONE of the commands below before doing anything else.
4
-
5
- Determine the user's intent and run the matching command NOW:
6
-
7
- 1. **Pick up work from the backlog** → `{{bin}} pick task`
8
- User wants to start working. Examples: "work", "go", "pick a task", "what's next?"
9
-
10
- 2. **Implement a specific task** → `{{bin}} focus "<task name>"`
11
- User mentions a particular task by name. Examples: "implement the auth task", "work on caching"
12
-
13
- 3. **Capture a new task** → `{{bin}} new task`
14
- User has concrete work to add. Keywords: "task: ..." or "add a task ..."
15
-
16
- 4. **Capture a new goal** → `{{bin}} new goal`
17
- User has a higher-level objective to add. Keywords: "goal: ..." or "add a goal ..."
18
-
19
- 5. **Capture a vague idea** → `{{bin}} new idea`
20
- User has a rough idea that might become work later. Keywords: "idea: ..." or "add an idea ..."
21
-
22
- 6. **Unclear** → `{{bin}} help`
23
- If none of the above clearly apply, run this to see all available commands.
24
-
25
- Do NOT proceed without running one of these commands.
26
- {{#if agentInstructions}}
27
-
28
- ---
29
-
30
- {{agentInstructions}}
31
- {{/if}}
@@ -1,26 +0,0 @@
1
- ## Implement a Task
2
-
3
- {{#if isClaudeCodeWeb}}Follow these steps. Use a todo list to track your progress.
4
- {{/if}}{{#unless isClaudeCodeWeb}}Follow these steps:
5
- {{/unless}}
6
- 1. Run `{{bin}} next` to identify the (unblocked) task the user is referring to
7
- 2. Run `{{bin}} focus "<task name>"` (so everyone knows you're working on it)
8
- 3. Run `{{bin}} check` to verify the project is in a good state
9
- 4. Implement the task
10
- {{#unless hooksInstalled}}5. Run `{{bin}} check` before committing
11
- 6.{{/unless}}{{#if hooksInstalled}}5.{{/if}} Create a single atomic commit that includes:
12
- - All implementation changes
13
- - Deletion of the completed task file
14
- - Updates to any facts that changed
15
- - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)
16
-
17
- Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.
18
-
19
- Example: If the task title is "Add validation for user input", the commit message should be:
20
- ```
21
- Add validation for user input
22
- ```
23
-
24
- {{#unless hooksInstalled}}7.{{/unless}}{{#if hooksInstalled}}6.{{/if}} Push your commit to the remote repository
25
-
26
- Keep your change small and focused. One task, one commit.
@@ -1,22 +0,0 @@
1
- ## Adding a New Goal
2
-
3
- Goals are guiding principles that persist across tasks. They define the "why" behind the work.
4
-
5
- {{#if isClaudeCodeWeb}}Follow these steps. Use a todo list to track your progress.
6
- {{/if}}{{#unless isClaudeCodeWeb}}Follow these steps:
7
- {{/unless}}
8
- 1. Run `{{bin}} goals` to see existing goals and avoid duplication
9
- 2. Create a new markdown file in `.dust/goals/` with a descriptive kebab-case name (e.g., `cross-platform-support.md`)
10
- 3. Add a title as the first line using an H1 heading (e.g., `# Cross-platform support`)
11
- 4. Write a clear description explaining:
12
- - What this goal means in practice
13
- - Why it matters for the project
14
- - How to evaluate whether work supports this goal
15
- 5. Run `{{bin}} lint markdown` to catch any formatting issues
16
- 6. Create a single atomic commit with a message in the format "Add goal: <title>"
17
- 7. Push your commit to the remote repository
18
-
19
- Goals should be:
20
- - **Stable** - They rarely change once established
21
- - **Actionable** - Tasks can be linked to them
22
- - **Clear** - Anyone reading should understand what it means
@@ -1,46 +0,0 @@
1
- ## Adding a New Idea
2
-
3
- Follow these steps:
4
-
5
- 1. Run `{{bin}} ideas` to see all existing ideas and avoid duplicates
6
- 2. Create a new markdown file in `.dust/ideas/` with a descriptive kebab-case name (e.g., `improve-error-messages.md`)
7
- 3. Add a title as the first line using an H1 heading (e.g., `# Improve error messages`)
8
- 4. Write a brief description of the potential change or improvement
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.)
@@ -1,7 +0,0 @@
1
- ## Pick a Task
2
-
3
- Follow these steps:
4
-
5
- 1. Run `{{bin}} next` to see available tasks
6
- 2. Pick ONE task and read its file to understand the requirements
7
- 3. Run `{{bin}} implement task` for instructions about how to implement
@@ -1,5 +0,0 @@
1
- # Agent Instructions
2
-
3
- This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4
-
5
- Always immediately run `{{dustCommand}} agent` when you start working in this repository.
@@ -1,30 +0,0 @@
1
- # Dead Code
2
-
3
- Find and remove unused code to improve maintainability and reduce bundle size.
4
-
5
- ## Scope
6
-
7
- Focus on these areas:
8
-
9
- 1. **Unused exports** - Functions, classes, constants that are never imported
10
- 2. **Unreachable code** - Code after return statements, impossible conditions
11
- 3. **Orphaned files** - Files that are not imported anywhere
12
- 4. **Unused dependencies** - Packages in package.json not used in code
13
- 5. **Commented-out code** - Old code left in comments
14
-
15
- ## Goals
16
-
17
- (none)
18
-
19
- ## Blocked By
20
-
21
- (none)
22
-
23
- ## Definition of Done
24
-
25
- - [ ] Ran static analysis tools to find unused exports
26
- - [ ] Identified files with no incoming imports
27
- - [ ] Listed unused dependencies
28
- - [ ] Reviewed commented-out code blocks
29
- - [ ] Created list of code safe to remove
30
- - [ ] Verified removal won't break dynamic imports or reflection
@@ -1,30 +0,0 @@
1
- # Security Review
2
-
3
- Review the codebase for common security vulnerabilities and misconfigurations.
4
-
5
- ## Scope
6
-
7
- Focus on these areas:
8
-
9
- 1. **Hardcoded secrets** - API keys, passwords, tokens in source code
10
- 2. **Injection vulnerabilities** - SQL injection, command injection, XSS
11
- 3. **Authentication issues** - Weak password handling, missing auth checks
12
- 4. **Sensitive data exposure** - Logging sensitive data, insecure storage
13
- 5. **Dependency vulnerabilities** - Known CVEs in dependencies
14
-
15
- ## Goals
16
-
17
- (none)
18
-
19
- ## Blocked By
20
-
21
- (none)
22
-
23
- ## Definition of Done
24
-
25
- - [ ] Searched for hardcoded secrets (API keys, passwords, tokens)
26
- - [ ] Reviewed input validation and sanitization
27
- - [ ] Checked authentication and authorization logic
28
- - [ ] Verified sensitive data is not logged or exposed
29
- - [ ] Ran dependency audit for known vulnerabilities
30
- - [ ] Documented any findings with severity ratings
@@ -1,29 +0,0 @@
1
- # Test Coverage
2
-
3
- Identify untested code paths and areas that need additional test coverage.
4
-
5
- ## Scope
6
-
7
- Focus on these areas:
8
-
9
- 1. **Core business logic** - Functions that handle critical operations
10
- 2. **Edge cases** - Boundary conditions, error handling paths
11
- 3. **Integration points** - API endpoints, database operations
12
- 4. **User-facing features** - UI components, form validation
13
- 5. **Recent changes** - Code modified in the last few commits
14
-
15
- ## Goals
16
-
17
- (none)
18
-
19
- ## Blocked By
20
-
21
- (none)
22
-
23
- ## Definition of Done
24
-
25
- - [ ] Identified modules with low or no test coverage
26
- - [ ] Listed critical paths that lack tests
27
- - [ ] Prioritized areas by risk and importance
28
- - [ ] Created list of specific test cases to add
29
- - [ ] Estimated effort for improving coverage
@@ -1,5 +0,0 @@
1
- # Claude Code Instructions
2
-
3
- This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
4
-
5
- Always immediately run `{{dustCommand}} agent` when you start working in this repository.
@@ -1,36 +0,0 @@
1
- 💨 dust - Flow state for AI coding agents.
2
-
3
- Usage: {{bin}} <command> [options]
4
-
5
- Commands:
6
- init Initialize a new Dust repository
7
- lint markdown Run lint checks on .dust/ files
8
- list List all items (tasks, ideas, goals, facts)
9
- tasks List tasks (actionable work with definitions of done)
10
- ideas List ideas (vague proposals, convert to tasks when ready)
11
- goals List goals (guiding principles, stable, rarely change)
12
- facts List facts (documentation of current system state)
13
- next Show tasks ready to work on (not blocked)
14
- check Run project-defined quality gate hook
15
- agent Agent greeting and routing instructions
16
- focus Declare current objective (for remote session tracking)
17
- pick task Pick the next task to work on
18
- implement task Implement a task
19
- new task Create a new task
20
- new goal Create a new goal
21
- new idea Create a new idea
22
- loop claude Run continuous Claude iteration on tasks
23
- pre push Git pre-push hook validation
24
- help Show this help message
25
-
26
- 🤖 Agent Guide
27
-
28
- Dust is a lightweight planning system. The .dust/ directory contains:
29
- - goals/ - Guiding principles (stable, rarely change)
30
- - ideas/ - Proposals (convert to tasks when ready)
31
- - tasks/ - Actionable work with definitions of done
32
- - facts/ - Documentation of current system state
33
-
34
- Workflow: Pick a task → implement it → delete the task file → commit atomically.
35
-
36
- Run `{{bin}} agent` to get started!