@joshski/dust 0.1.13 → 0.1.14

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/dust.js +284 -23
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Flow state for AI coding agents.**
4
4
 
5
- Dust provides a CLI that agents use to systematically blitz through your work queue.
5
+ Dust provides a CLI that agents use to systematically blaze through your backlog.
6
6
 
7
7
  [![CI](https://github.com/joshski/dust/actions/workflows/ci.yml/badge.svg)](https://github.com/joshski/dust/actions/workflows/ci.yml)
8
8
 
package/dist/dust.js CHANGED
@@ -97,6 +97,20 @@ function loadTemplate(name, variables = {}) {
97
97
  return content;
98
98
  }
99
99
 
100
+ // lib/agents/detection.ts
101
+ function detectAgent(env = process.env) {
102
+ if (env.CLAUDECODE) {
103
+ if (env.CLAUDE_CODE_ENTRYPOINT === "remote") {
104
+ return { type: "claude-code-web", name: "Claude Code Web" };
105
+ }
106
+ return { type: "claude-code", name: "Claude Code" };
107
+ }
108
+ if (env.CODEX_HOME) {
109
+ return { type: "codex", name: "Codex" };
110
+ }
111
+ return { type: "unknown", name: "Agent" };
112
+ }
113
+
100
114
  // lib/git/hooks.ts
101
115
  import { join as join3 } from "node:path";
102
116
  var DUST_HOOK_START = "# BEGIN DUST HOOK";
@@ -220,25 +234,13 @@ ${newHookContent}
220
234
  }
221
235
 
222
236
  // lib/cli/commands/agent-shared.ts
223
- function detectAgent(env = process.env) {
224
- if (env.CLAUDECODE) {
225
- if (env.CLAUDE_CODE_ENTRYPOINT === "remote") {
226
- return "Claude Code Web";
227
- }
228
- return "Claude Code";
229
- }
230
- if (env.CODEX_HOME) {
231
- return "Codex";
232
- }
233
- return "Agent";
234
- }
235
237
  function templateVariables(settings, hooksInstalled, env = process.env) {
236
- const agentName = detectAgent(env);
238
+ const agent = detectAgent(env);
237
239
  return {
238
240
  bin: settings.dustCommand,
239
- agentName,
241
+ agentName: agent.name,
240
242
  hooksInstalled: hooksInstalled ? "true" : "false",
241
- isClaudeCodeWeb: agentName === "Claude Code Web" ? "true" : ""
243
+ isClaudeCodeWeb: agent.type === "claude-code-web" ? "true" : ""
242
244
  };
243
245
  }
244
246
  async function manageGitHooks(dependencies) {
@@ -330,6 +332,7 @@ function extractOpeningSentence(content) {
330
332
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked by", "## Definition of done"];
331
333
  var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
332
334
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
335
+ var MAX_OPENING_SENTENCE_LENGTH = 150;
333
336
  function validateFilename(filePath) {
334
337
  const parts = filePath.split("/");
335
338
  const filename = parts[parts.length - 1];
@@ -370,6 +373,19 @@ function validateOpeningSentence(filePath, content) {
370
373
  }
371
374
  return null;
372
375
  }
376
+ function validateOpeningSentenceLength(filePath, content) {
377
+ const openingSentence = extractOpeningSentence(content);
378
+ if (!openingSentence) {
379
+ return null;
380
+ }
381
+ if (openingSentence.length > MAX_OPENING_SENTENCE_LENGTH) {
382
+ return {
383
+ file: filePath,
384
+ message: `Opening sentence is ${openingSentence.length} characters (max ${MAX_OPENING_SENTENCE_LENGTH}). Split into multiple sentences; only the first sentence is checked.`
385
+ };
386
+ }
387
+ return null;
388
+ }
373
389
  function validateTaskHeadings(filePath, content) {
374
390
  const violations = [];
375
391
  for (const heading of REQUIRED_HEADINGS) {
@@ -662,6 +678,10 @@ async function lintMarkdown(dependencies) {
662
678
  if (openingSentenceViolation) {
663
679
  violations.push(openingSentenceViolation);
664
680
  }
681
+ const openingSentenceLengthViolation = validateOpeningSentenceLength(filePath, content);
682
+ if (openingSentenceLengthViolation) {
683
+ violations.push(openingSentenceLengthViolation);
684
+ }
665
685
  const titleFilenameViolation = validateTitleFilenameMatch(filePath, content);
666
686
  if (titleFilenameViolation) {
667
687
  violations.push(titleFilenameViolation);
@@ -1202,7 +1222,216 @@ function* parseRawEvent(raw) {
1202
1222
  }
1203
1223
  }
1204
1224
 
1225
+ // lib/claude/tool-formatters.ts
1226
+ var DIVIDER = "────────────────────────────────";
1227
+ function formatWrite(input) {
1228
+ const filePath = input.file_path;
1229
+ const content = input.content;
1230
+ const others = getUnrecognizedArgs(input, ["file_path", "content"]);
1231
+ const lines = [];
1232
+ lines.push(`\uD83D\uDD27 Write: ${filePath ?? "(unknown)"}`);
1233
+ lines.push(DIVIDER);
1234
+ if (content !== undefined) {
1235
+ for (const line of content.split(`
1236
+ `)) {
1237
+ lines.push(line);
1238
+ }
1239
+ }
1240
+ lines.push(DIVIDER);
1241
+ appendOtherArgs(lines, others);
1242
+ return lines;
1243
+ }
1244
+ function formatEdit(input) {
1245
+ const filePath = input.file_path;
1246
+ const oldString = input.old_string;
1247
+ const newString = input.new_string;
1248
+ const others = getUnrecognizedArgs(input, [
1249
+ "file_path",
1250
+ "old_string",
1251
+ "new_string",
1252
+ "replace_all"
1253
+ ]);
1254
+ const lines = [];
1255
+ lines.push(`\uD83D\uDD27 Edit: ${filePath ?? "(unknown)"}`);
1256
+ lines.push("Replace:");
1257
+ lines.push(DIVIDER);
1258
+ if (oldString !== undefined) {
1259
+ for (const line of oldString.split(`
1260
+ `)) {
1261
+ lines.push(line);
1262
+ }
1263
+ }
1264
+ lines.push(DIVIDER);
1265
+ lines.push("With:");
1266
+ lines.push(DIVIDER);
1267
+ if (newString !== undefined) {
1268
+ for (const line of newString.split(`
1269
+ `)) {
1270
+ lines.push(line);
1271
+ }
1272
+ }
1273
+ lines.push(DIVIDER);
1274
+ appendOtherArgs(lines, others);
1275
+ return lines;
1276
+ }
1277
+ function formatRead(input) {
1278
+ const filePath = input.file_path;
1279
+ const offset = input.offset;
1280
+ const limit = input.limit;
1281
+ const others = getUnrecognizedArgs(input, ["file_path", "offset", "limit"]);
1282
+ const lines = [];
1283
+ let lineRange = "";
1284
+ if (offset !== undefined || limit !== undefined) {
1285
+ const start = offset ?? 1;
1286
+ const end = limit !== undefined ? start + limit - 1 : undefined;
1287
+ lineRange = end !== undefined ? ` (lines ${start}-${end})` : ` (from line ${start})`;
1288
+ }
1289
+ lines.push(`\uD83D\uDD27 Read: ${filePath ?? "(unknown)"}${lineRange}`);
1290
+ appendOtherArgs(lines, others);
1291
+ return lines;
1292
+ }
1293
+ function formatBash(input) {
1294
+ const command = input.command;
1295
+ const description = input.description;
1296
+ const others = getUnrecognizedArgs(input, [
1297
+ "command",
1298
+ "description",
1299
+ "timeout",
1300
+ "run_in_background",
1301
+ "dangerouslyDisableSandbox",
1302
+ "_simulatedSedEdit"
1303
+ ]);
1304
+ const lines = [];
1305
+ const header = description ?? "Run command";
1306
+ lines.push(`\uD83D\uDD27 Bash: ${header}`);
1307
+ if (command !== undefined) {
1308
+ lines.push(`$ ${command}`);
1309
+ }
1310
+ appendOtherArgs(lines, others);
1311
+ return lines;
1312
+ }
1313
+ function formatTodoWrite(input) {
1314
+ const todos = input.todos;
1315
+ const others = getUnrecognizedArgs(input, ["todos"]);
1316
+ const lines = [];
1317
+ const count = todos?.length ?? 0;
1318
+ lines.push(`\uD83D\uDD27 TodoWrite: ${count} item${count === 1 ? "" : "s"}`);
1319
+ if (todos) {
1320
+ for (const todo of todos) {
1321
+ const icon = todo.status === "completed" ? "☑" : "☐";
1322
+ lines.push(`${icon} ${todo.content}`);
1323
+ }
1324
+ }
1325
+ appendOtherArgs(lines, others);
1326
+ return lines;
1327
+ }
1328
+ function formatGrep(input) {
1329
+ const pattern = input.pattern;
1330
+ const path = input.path;
1331
+ const glob = input.glob;
1332
+ const type = input.type;
1333
+ const others = getUnrecognizedArgs(input, [
1334
+ "pattern",
1335
+ "path",
1336
+ "glob",
1337
+ "type",
1338
+ "output_mode",
1339
+ "context",
1340
+ "-A",
1341
+ "-B",
1342
+ "-C",
1343
+ "-i",
1344
+ "-n",
1345
+ "head_limit",
1346
+ "offset",
1347
+ "multiline"
1348
+ ]);
1349
+ const lines = [];
1350
+ const location = path ?? ".";
1351
+ let filter = "";
1352
+ if (glob) {
1353
+ filter = ` (${glob})`;
1354
+ } else if (type) {
1355
+ filter = ` (type: ${type})`;
1356
+ }
1357
+ lines.push(`\uD83D\uDD27 Grep: "${pattern ?? ""}" in ${location}${filter}`);
1358
+ appendOtherArgs(lines, others);
1359
+ return lines;
1360
+ }
1361
+ function formatGlob(input) {
1362
+ const pattern = input.pattern;
1363
+ const path = input.path;
1364
+ const others = getUnrecognizedArgs(input, ["pattern", "path"]);
1365
+ const lines = [];
1366
+ const location = path ?? ".";
1367
+ lines.push(`\uD83D\uDD27 Glob: ${pattern ?? ""} in ${location}`);
1368
+ appendOtherArgs(lines, others);
1369
+ return lines;
1370
+ }
1371
+ function formatTask(input) {
1372
+ const description = input.description;
1373
+ const subagentType = input.subagent_type;
1374
+ const prompt = input.prompt;
1375
+ const others = getUnrecognizedArgs(input, [
1376
+ "description",
1377
+ "subagent_type",
1378
+ "prompt",
1379
+ "model",
1380
+ "max_turns",
1381
+ "resume",
1382
+ "run_in_background"
1383
+ ]);
1384
+ const lines = [];
1385
+ const header = description ?? subagentType ?? "task";
1386
+ lines.push(`\uD83D\uDD27 Task: ${header}`);
1387
+ if (prompt !== undefined) {
1388
+ const truncated = prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
1389
+ lines.push(`"${truncated}"`);
1390
+ }
1391
+ appendOtherArgs(lines, others);
1392
+ return lines;
1393
+ }
1394
+ function formatFallback(name, input) {
1395
+ const lines = [];
1396
+ lines.push(`\uD83D\uDD27 Tool: ${name}`);
1397
+ lines.push(`Input: ${JSON.stringify(input, null, 2)}`);
1398
+ return lines;
1399
+ }
1400
+ var formatters = {
1401
+ Write: formatWrite,
1402
+ Edit: formatEdit,
1403
+ Read: formatRead,
1404
+ Bash: formatBash,
1405
+ TodoWrite: formatTodoWrite,
1406
+ Grep: formatGrep,
1407
+ Glob: formatGlob,
1408
+ Task: formatTask
1409
+ };
1410
+ function formatToolUse(name, input) {
1411
+ const formatter = formatters[name];
1412
+ if (formatter) {
1413
+ return formatter(input);
1414
+ }
1415
+ return formatFallback(name, input);
1416
+ }
1417
+ function getUnrecognizedArgs(input, knownKeys) {
1418
+ const others = {};
1419
+ for (const key of Object.keys(input)) {
1420
+ if (!knownKeys.includes(key)) {
1421
+ others[key] = input[key];
1422
+ }
1423
+ }
1424
+ return others;
1425
+ }
1426
+ function appendOtherArgs(lines, others) {
1427
+ if (Object.keys(others).length > 0) {
1428
+ lines.push("");
1429
+ lines.push(`(Other arguments: ${JSON.stringify(others)})`);
1430
+ }
1431
+ }
1432
+
1205
1433
  // lib/claude/streamer.ts
1434
+ var DIVIDER2 = "────────────────────────────────";
1206
1435
  async function streamEvents(events, sink) {
1207
1436
  let hadTextOutput = false;
1208
1437
  for await (const raw of events) {
@@ -1226,12 +1455,15 @@ function processEvent(event, sink, state) {
1226
1455
  sink.line("");
1227
1456
  sink.line("");
1228
1457
  }
1229
- sink.line(`\uD83D\uDD27 Tool: ${event.name}`);
1230
- sink.line(` Input: ${JSON.stringify(event.input, null, 2).replace(/\n/g, `
1231
- `)}`);
1458
+ for (const line of formatToolUse(event.name, event.input)) {
1459
+ sink.line(line);
1460
+ }
1232
1461
  break;
1233
1462
  case "tool_result":
1234
- sink.line(`✅ Result (${event.content.length} chars)`);
1463
+ sink.line("Result:");
1464
+ sink.line(DIVIDER2);
1465
+ sink.line(event.content);
1466
+ sink.line(DIVIDER2);
1235
1467
  sink.line("");
1236
1468
  break;
1237
1469
  case "result":
@@ -1379,7 +1611,33 @@ async function runOneIteration(dependencies, loopDependencies) {
1379
1611
  context.stdout("\uD83D\uDD04 Syncing with remote...");
1380
1612
  const pullResult = await gitPull(context.cwd, spawn2);
1381
1613
  if (!pullResult.success) {
1382
- context.stdout(`Note: git pull skipped (${pullResult.message})`);
1614
+ context.stdout(`⚠️ git pull failed: ${pullResult.message}`);
1615
+ context.stdout("");
1616
+ context.stdout("\uD83E\uDD16 Starting Claude to resolve the conflict...");
1617
+ context.stdout("");
1618
+ const prompt = `git pull failed with the following error:
1619
+
1620
+ ${pullResult.message}
1621
+
1622
+ Please resolve this issue. Common approaches:
1623
+ 1. If there are merge conflicts, resolve them
1624
+ 2. If local commits need to be rebased, use git rebase
1625
+ 3. After resolving, commit any changes and push to remote
1626
+
1627
+ Make sure the repository is in a clean state and synced with remote before finishing.`;
1628
+ try {
1629
+ await run2(prompt, { cwd: context.cwd, dangerouslySkipPermissions: true });
1630
+ context.stdout("");
1631
+ context.stdout("✅ Claude resolved the git pull conflict. Continuing loop...");
1632
+ context.stdout("");
1633
+ return "resolved_pull_conflict";
1634
+ } catch (error) {
1635
+ const message = error instanceof Error ? error.message : String(error);
1636
+ context.stderr(`Claude failed to resolve git pull conflict: ${message}`);
1637
+ context.stdout("");
1638
+ context.stdout("⚠️ Continuing loop despite unresolved conflict...");
1639
+ context.stdout("");
1640
+ }
1383
1641
  }
1384
1642
  context.stdout("\uD83D\uDD0D Checking for available tasks...");
1385
1643
  const hasTasks = await hasAvailableTasks(dependencies);
@@ -1388,7 +1646,9 @@ async function runOneIteration(dependencies, loopDependencies) {
1388
1646
  context.stdout("");
1389
1647
  return "no_tasks";
1390
1648
  }
1391
- context.stdout("✨ Found task(s). \uD83E\uDD16 Starting Claude...");
1649
+ context.stdout("✨ Found a task!");
1650
+ context.stdout("");
1651
+ context.stdout("\uD83E\uDD16 Starting Claude...");
1392
1652
  context.stdout("");
1393
1653
  try {
1394
1654
  await run2("go", { cwd: context.cwd, dangerouslySkipPermissions: true });
@@ -1535,12 +1795,13 @@ async function getChangesFromRemote(cwd, gitRunner) {
1535
1795
  }
1536
1796
  return parseGitDiffNameStatus(diffResult.output);
1537
1797
  }
1538
- async function prePush(dependencies, gitRunner = defaultGitRunner) {
1798
+ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process.env) {
1539
1799
  const { context } = dependencies;
1540
1800
  const changes = await getChangesFromRemote(context.cwd, gitRunner);
1541
1801
  if (changes.length > 0) {
1542
1802
  const analysis = analyzeChangesForTaskOnlyPattern(changes);
1543
- if (analysis.isTaskOnly) {
1803
+ const agent2 = detectAgent(env);
1804
+ if (analysis.isTaskOnly && agent2.type === "claude-code-web") {
1544
1805
  context.stderr("");
1545
1806
  context.stderr("⚠️ Task-only commit detected! You added a task but did not implement it.");
1546
1807
  context.stderr("");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.13",
4
- "description": "A lightweight planning system for human-AI collaboration",
3
+ "version": "0.1.14",
4
+ "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dust": "./dist/dust.js"