@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.
- package/README.md +1 -1
- package/dist/dust.js +284 -23
- 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
|
|
5
|
+
Dust provides a CLI that agents use to systematically blaze through your backlog.
|
|
6
6
|
|
|
7
7
|
[](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
|
|
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:
|
|
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
|
-
|
|
1230
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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