@joshski/dust 0.1.13 → 0.1.15
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 +420 -49
- 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);
|
|
@@ -1101,7 +1121,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
|
|
|
1101
1121
|
model,
|
|
1102
1122
|
systemPrompt,
|
|
1103
1123
|
sessionId,
|
|
1104
|
-
dangerouslySkipPermissions
|
|
1124
|
+
dangerouslySkipPermissions,
|
|
1125
|
+
env
|
|
1105
1126
|
} = options;
|
|
1106
1127
|
const claudeArguments = [
|
|
1107
1128
|
"-p",
|
|
@@ -1131,7 +1152,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
|
|
|
1131
1152
|
}
|
|
1132
1153
|
const proc = dependencies.spawn("claude", claudeArguments, {
|
|
1133
1154
|
cwd,
|
|
1134
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1155
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1156
|
+
env: { ...process.env, ...env }
|
|
1135
1157
|
});
|
|
1136
1158
|
if (!proc.stdout) {
|
|
1137
1159
|
throw new Error("Failed to get stdout from claude process");
|
|
@@ -1202,7 +1224,216 @@ function* parseRawEvent(raw) {
|
|
|
1202
1224
|
}
|
|
1203
1225
|
}
|
|
1204
1226
|
|
|
1227
|
+
// lib/claude/tool-formatters.ts
|
|
1228
|
+
var DIVIDER = "────────────────────────────────";
|
|
1229
|
+
function formatWrite(input) {
|
|
1230
|
+
const filePath = input.file_path;
|
|
1231
|
+
const content = input.content;
|
|
1232
|
+
const others = getUnrecognizedArgs(input, ["file_path", "content"]);
|
|
1233
|
+
const lines = [];
|
|
1234
|
+
lines.push(`\uD83D\uDD27 Write: ${filePath ?? "(unknown)"}`);
|
|
1235
|
+
lines.push(DIVIDER);
|
|
1236
|
+
if (content !== undefined) {
|
|
1237
|
+
for (const line of content.split(`
|
|
1238
|
+
`)) {
|
|
1239
|
+
lines.push(line);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
lines.push(DIVIDER);
|
|
1243
|
+
appendOtherArgs(lines, others);
|
|
1244
|
+
return lines;
|
|
1245
|
+
}
|
|
1246
|
+
function formatEdit(input) {
|
|
1247
|
+
const filePath = input.file_path;
|
|
1248
|
+
const oldString = input.old_string;
|
|
1249
|
+
const newString = input.new_string;
|
|
1250
|
+
const others = getUnrecognizedArgs(input, [
|
|
1251
|
+
"file_path",
|
|
1252
|
+
"old_string",
|
|
1253
|
+
"new_string",
|
|
1254
|
+
"replace_all"
|
|
1255
|
+
]);
|
|
1256
|
+
const lines = [];
|
|
1257
|
+
lines.push(`\uD83D\uDD27 Edit: ${filePath ?? "(unknown)"}`);
|
|
1258
|
+
lines.push("Replace:");
|
|
1259
|
+
lines.push(DIVIDER);
|
|
1260
|
+
if (oldString !== undefined) {
|
|
1261
|
+
for (const line of oldString.split(`
|
|
1262
|
+
`)) {
|
|
1263
|
+
lines.push(line);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
lines.push(DIVIDER);
|
|
1267
|
+
lines.push("With:");
|
|
1268
|
+
lines.push(DIVIDER);
|
|
1269
|
+
if (newString !== undefined) {
|
|
1270
|
+
for (const line of newString.split(`
|
|
1271
|
+
`)) {
|
|
1272
|
+
lines.push(line);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
lines.push(DIVIDER);
|
|
1276
|
+
appendOtherArgs(lines, others);
|
|
1277
|
+
return lines;
|
|
1278
|
+
}
|
|
1279
|
+
function formatRead(input) {
|
|
1280
|
+
const filePath = input.file_path;
|
|
1281
|
+
const offset = input.offset;
|
|
1282
|
+
const limit = input.limit;
|
|
1283
|
+
const others = getUnrecognizedArgs(input, ["file_path", "offset", "limit"]);
|
|
1284
|
+
const lines = [];
|
|
1285
|
+
let lineRange = "";
|
|
1286
|
+
if (offset !== undefined || limit !== undefined) {
|
|
1287
|
+
const start = offset ?? 1;
|
|
1288
|
+
const end = limit !== undefined ? start + limit - 1 : undefined;
|
|
1289
|
+
lineRange = end !== undefined ? ` (lines ${start}-${end})` : ` (from line ${start})`;
|
|
1290
|
+
}
|
|
1291
|
+
lines.push(`\uD83D\uDD27 Read: ${filePath ?? "(unknown)"}${lineRange}`);
|
|
1292
|
+
appendOtherArgs(lines, others);
|
|
1293
|
+
return lines;
|
|
1294
|
+
}
|
|
1295
|
+
function formatBash(input) {
|
|
1296
|
+
const command = input.command;
|
|
1297
|
+
const description = input.description;
|
|
1298
|
+
const others = getUnrecognizedArgs(input, [
|
|
1299
|
+
"command",
|
|
1300
|
+
"description",
|
|
1301
|
+
"timeout",
|
|
1302
|
+
"run_in_background",
|
|
1303
|
+
"dangerouslyDisableSandbox",
|
|
1304
|
+
"_simulatedSedEdit"
|
|
1305
|
+
]);
|
|
1306
|
+
const lines = [];
|
|
1307
|
+
const header = description ?? "Run command";
|
|
1308
|
+
lines.push(`\uD83D\uDD27 Bash: ${header}`);
|
|
1309
|
+
if (command !== undefined) {
|
|
1310
|
+
lines.push(`$ ${command}`);
|
|
1311
|
+
}
|
|
1312
|
+
appendOtherArgs(lines, others);
|
|
1313
|
+
return lines;
|
|
1314
|
+
}
|
|
1315
|
+
function formatTodoWrite(input) {
|
|
1316
|
+
const todos = input.todos;
|
|
1317
|
+
const others = getUnrecognizedArgs(input, ["todos"]);
|
|
1318
|
+
const lines = [];
|
|
1319
|
+
const count = todos?.length ?? 0;
|
|
1320
|
+
lines.push(`\uD83D\uDD27 TodoWrite: ${count} item${count === 1 ? "" : "s"}`);
|
|
1321
|
+
if (todos) {
|
|
1322
|
+
for (const todo of todos) {
|
|
1323
|
+
const icon = todo.status === "completed" ? "☑" : "☐";
|
|
1324
|
+
lines.push(`${icon} ${todo.content}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
appendOtherArgs(lines, others);
|
|
1328
|
+
return lines;
|
|
1329
|
+
}
|
|
1330
|
+
function formatGrep(input) {
|
|
1331
|
+
const pattern = input.pattern;
|
|
1332
|
+
const path = input.path;
|
|
1333
|
+
const glob = input.glob;
|
|
1334
|
+
const type = input.type;
|
|
1335
|
+
const others = getUnrecognizedArgs(input, [
|
|
1336
|
+
"pattern",
|
|
1337
|
+
"path",
|
|
1338
|
+
"glob",
|
|
1339
|
+
"type",
|
|
1340
|
+
"output_mode",
|
|
1341
|
+
"context",
|
|
1342
|
+
"-A",
|
|
1343
|
+
"-B",
|
|
1344
|
+
"-C",
|
|
1345
|
+
"-i",
|
|
1346
|
+
"-n",
|
|
1347
|
+
"head_limit",
|
|
1348
|
+
"offset",
|
|
1349
|
+
"multiline"
|
|
1350
|
+
]);
|
|
1351
|
+
const lines = [];
|
|
1352
|
+
const location = path ?? ".";
|
|
1353
|
+
let filter = "";
|
|
1354
|
+
if (glob) {
|
|
1355
|
+
filter = ` (${glob})`;
|
|
1356
|
+
} else if (type) {
|
|
1357
|
+
filter = ` (type: ${type})`;
|
|
1358
|
+
}
|
|
1359
|
+
lines.push(`\uD83D\uDD27 Grep: "${pattern ?? ""}" in ${location}${filter}`);
|
|
1360
|
+
appendOtherArgs(lines, others);
|
|
1361
|
+
return lines;
|
|
1362
|
+
}
|
|
1363
|
+
function formatGlob(input) {
|
|
1364
|
+
const pattern = input.pattern;
|
|
1365
|
+
const path = input.path;
|
|
1366
|
+
const others = getUnrecognizedArgs(input, ["pattern", "path"]);
|
|
1367
|
+
const lines = [];
|
|
1368
|
+
const location = path ?? ".";
|
|
1369
|
+
lines.push(`\uD83D\uDD27 Glob: ${pattern ?? ""} in ${location}`);
|
|
1370
|
+
appendOtherArgs(lines, others);
|
|
1371
|
+
return lines;
|
|
1372
|
+
}
|
|
1373
|
+
function formatTask(input) {
|
|
1374
|
+
const description = input.description;
|
|
1375
|
+
const subagentType = input.subagent_type;
|
|
1376
|
+
const prompt = input.prompt;
|
|
1377
|
+
const others = getUnrecognizedArgs(input, [
|
|
1378
|
+
"description",
|
|
1379
|
+
"subagent_type",
|
|
1380
|
+
"prompt",
|
|
1381
|
+
"model",
|
|
1382
|
+
"max_turns",
|
|
1383
|
+
"resume",
|
|
1384
|
+
"run_in_background"
|
|
1385
|
+
]);
|
|
1386
|
+
const lines = [];
|
|
1387
|
+
const header = description ?? subagentType ?? "task";
|
|
1388
|
+
lines.push(`\uD83D\uDD27 Task: ${header}`);
|
|
1389
|
+
if (prompt !== undefined) {
|
|
1390
|
+
const truncated = prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
|
|
1391
|
+
lines.push(`"${truncated}"`);
|
|
1392
|
+
}
|
|
1393
|
+
appendOtherArgs(lines, others);
|
|
1394
|
+
return lines;
|
|
1395
|
+
}
|
|
1396
|
+
function formatFallback(name, input) {
|
|
1397
|
+
const lines = [];
|
|
1398
|
+
lines.push(`\uD83D\uDD27 Tool: ${name}`);
|
|
1399
|
+
lines.push(`Input: ${JSON.stringify(input, null, 2)}`);
|
|
1400
|
+
return lines;
|
|
1401
|
+
}
|
|
1402
|
+
var formatters = {
|
|
1403
|
+
Write: formatWrite,
|
|
1404
|
+
Edit: formatEdit,
|
|
1405
|
+
Read: formatRead,
|
|
1406
|
+
Bash: formatBash,
|
|
1407
|
+
TodoWrite: formatTodoWrite,
|
|
1408
|
+
Grep: formatGrep,
|
|
1409
|
+
Glob: formatGlob,
|
|
1410
|
+
Task: formatTask
|
|
1411
|
+
};
|
|
1412
|
+
function formatToolUse(name, input) {
|
|
1413
|
+
const formatter = formatters[name];
|
|
1414
|
+
if (formatter) {
|
|
1415
|
+
return formatter(input);
|
|
1416
|
+
}
|
|
1417
|
+
return formatFallback(name, input);
|
|
1418
|
+
}
|
|
1419
|
+
function getUnrecognizedArgs(input, knownKeys) {
|
|
1420
|
+
const others = {};
|
|
1421
|
+
for (const key of Object.keys(input)) {
|
|
1422
|
+
if (!knownKeys.includes(key)) {
|
|
1423
|
+
others[key] = input[key];
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
return others;
|
|
1427
|
+
}
|
|
1428
|
+
function appendOtherArgs(lines, others) {
|
|
1429
|
+
if (Object.keys(others).length > 0) {
|
|
1430
|
+
lines.push("");
|
|
1431
|
+
lines.push(`(Other arguments: ${JSON.stringify(others)})`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1205
1435
|
// lib/claude/streamer.ts
|
|
1436
|
+
var DIVIDER2 = "────────────────────────────────";
|
|
1206
1437
|
async function streamEvents(events, sink) {
|
|
1207
1438
|
let hadTextOutput = false;
|
|
1208
1439
|
for await (const raw of events) {
|
|
@@ -1226,12 +1457,15 @@ function processEvent(event, sink, state) {
|
|
|
1226
1457
|
sink.line("");
|
|
1227
1458
|
sink.line("");
|
|
1228
1459
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1460
|
+
for (const line of formatToolUse(event.name, event.input)) {
|
|
1461
|
+
sink.line(line);
|
|
1462
|
+
}
|
|
1232
1463
|
break;
|
|
1233
1464
|
case "tool_result":
|
|
1234
|
-
sink.line(
|
|
1465
|
+
sink.line("Result:");
|
|
1466
|
+
sink.line(DIVIDER2);
|
|
1467
|
+
sink.line(event.content);
|
|
1468
|
+
sink.line(DIVIDER2);
|
|
1235
1469
|
sink.line("");
|
|
1236
1470
|
break;
|
|
1237
1471
|
case "result":
|
|
@@ -1331,11 +1565,71 @@ async function next(dependencies) {
|
|
|
1331
1565
|
}
|
|
1332
1566
|
|
|
1333
1567
|
// lib/cli/commands/loop.ts
|
|
1568
|
+
function formatEvent(event) {
|
|
1569
|
+
switch (event.type) {
|
|
1570
|
+
case "loop.warning":
|
|
1571
|
+
return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
|
|
1572
|
+
case "loop.started":
|
|
1573
|
+
return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
|
|
1574
|
+
case "loop.syncing":
|
|
1575
|
+
return "\uD83D\uDD04 Syncing with remote...";
|
|
1576
|
+
case "loop.sync_skipped":
|
|
1577
|
+
return `Note: git pull skipped (${event.reason})`;
|
|
1578
|
+
case "loop.checking_tasks":
|
|
1579
|
+
return "\uD83D\uDD0D Checking for available tasks...";
|
|
1580
|
+
case "loop.no_tasks":
|
|
1581
|
+
return "\uD83D\uDCA4 No tasks available. Sleeping...";
|
|
1582
|
+
case "loop.tasks_found":
|
|
1583
|
+
return "✨ Found task(s). \uD83E\uDD16 Starting Claude...";
|
|
1584
|
+
case "claude.started":
|
|
1585
|
+
return "\uD83E\uDD16 Claude session started";
|
|
1586
|
+
case "claude.ended":
|
|
1587
|
+
return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
|
|
1588
|
+
case "loop.iteration_complete":
|
|
1589
|
+
return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
|
|
1590
|
+
case "loop.ended":
|
|
1591
|
+
return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
async function defaultPostEvent(url, payload) {
|
|
1595
|
+
await fetch(url, {
|
|
1596
|
+
method: "POST",
|
|
1597
|
+
headers: { "Content-Type": "application/json" },
|
|
1598
|
+
body: JSON.stringify(payload)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1334
1601
|
function createDefaultDependencies() {
|
|
1335
1602
|
return {
|
|
1336
1603
|
spawn: nodeSpawn2,
|
|
1337
1604
|
run,
|
|
1338
|
-
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
1605
|
+
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
|
|
1606
|
+
postEvent: defaultPostEvent
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
function createEventPoster(eventsUrl, sessionId, postEvent, onError) {
|
|
1610
|
+
let sequence = 0;
|
|
1611
|
+
let currentAgentSessionId;
|
|
1612
|
+
return (event) => {
|
|
1613
|
+
if (!eventsUrl)
|
|
1614
|
+
return;
|
|
1615
|
+
sequence++;
|
|
1616
|
+
if (event.type === "claude.started") {
|
|
1617
|
+
currentAgentSessionId = crypto.randomUUID();
|
|
1618
|
+
}
|
|
1619
|
+
const payload = {
|
|
1620
|
+
sequence,
|
|
1621
|
+
timestamp: new Date().toISOString(),
|
|
1622
|
+
sessionId,
|
|
1623
|
+
event
|
|
1624
|
+
};
|
|
1625
|
+
if (event.type.startsWith("claude.") && currentAgentSessionId) {
|
|
1626
|
+
payload.agentSessionId = currentAgentSessionId;
|
|
1627
|
+
payload.agentType = "claude";
|
|
1628
|
+
}
|
|
1629
|
+
postEvent(eventsUrl, payload).catch(onError);
|
|
1630
|
+
if (event.type === "claude.ended") {
|
|
1631
|
+
currentAgentSessionId = undefined;
|
|
1632
|
+
}
|
|
1339
1633
|
};
|
|
1340
1634
|
}
|
|
1341
1635
|
var SLEEP_INTERVAL_MS = 30000;
|
|
@@ -1373,35 +1667,61 @@ async function hasAvailableTasks(dependencies) {
|
|
|
1373
1667
|
await next({ ...dependencies, context: captureContext });
|
|
1374
1668
|
return hasOutput;
|
|
1375
1669
|
}
|
|
1376
|
-
async function runOneIteration(dependencies, loopDependencies) {
|
|
1670
|
+
async function runOneIteration(dependencies, loopDependencies, emit) {
|
|
1377
1671
|
const { context } = dependencies;
|
|
1378
1672
|
const { spawn: spawn2, run: run2 } = loopDependencies;
|
|
1379
|
-
|
|
1673
|
+
emit({ type: "loop.syncing" });
|
|
1380
1674
|
const pullResult = await gitPull(context.cwd, spawn2);
|
|
1381
1675
|
if (!pullResult.success) {
|
|
1382
|
-
|
|
1676
|
+
emit({
|
|
1677
|
+
type: "loop.sync_skipped",
|
|
1678
|
+
reason: pullResult.message ?? "unknown error"
|
|
1679
|
+
});
|
|
1680
|
+
emit({ type: "claude.started" });
|
|
1681
|
+
const prompt = `git pull failed with the following error:
|
|
1682
|
+
|
|
1683
|
+
${pullResult.message}
|
|
1684
|
+
|
|
1685
|
+
Please resolve this issue. Common approaches:
|
|
1686
|
+
1. If there are merge conflicts, resolve them
|
|
1687
|
+
2. If local commits need to be rebased, use git rebase
|
|
1688
|
+
3. After resolving, commit any changes and push to remote
|
|
1689
|
+
|
|
1690
|
+
Make sure the repository is in a clean state and synced with remote before finishing.`;
|
|
1691
|
+
try {
|
|
1692
|
+
await run2(prompt, {
|
|
1693
|
+
cwd: context.cwd,
|
|
1694
|
+
dangerouslySkipPermissions: true,
|
|
1695
|
+
env: { DUST_UNATTENDED: "1" }
|
|
1696
|
+
});
|
|
1697
|
+
emit({ type: "claude.ended", success: true });
|
|
1698
|
+
return "resolved_pull_conflict";
|
|
1699
|
+
} catch (error) {
|
|
1700
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1701
|
+
context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
|
|
1702
|
+
emit({ type: "claude.ended", success: false, error: errorMessage });
|
|
1703
|
+
}
|
|
1383
1704
|
}
|
|
1384
|
-
|
|
1705
|
+
emit({ type: "loop.checking_tasks" });
|
|
1385
1706
|
const hasTasks = await hasAvailableTasks(dependencies);
|
|
1386
1707
|
if (!hasTasks) {
|
|
1387
|
-
|
|
1388
|
-
context.stdout("");
|
|
1708
|
+
emit({ type: "loop.no_tasks" });
|
|
1389
1709
|
return "no_tasks";
|
|
1390
1710
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1711
|
+
emit({ type: "loop.tasks_found" });
|
|
1712
|
+
emit({ type: "claude.started" });
|
|
1393
1713
|
try {
|
|
1394
|
-
await run2("go", {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1714
|
+
await run2("go", {
|
|
1715
|
+
cwd: context.cwd,
|
|
1716
|
+
dangerouslySkipPermissions: true,
|
|
1717
|
+
env: { DUST_UNATTENDED: "1" }
|
|
1718
|
+
});
|
|
1719
|
+
emit({ type: "claude.ended", success: true });
|
|
1398
1720
|
return "ran_claude";
|
|
1399
1721
|
} catch (error) {
|
|
1400
|
-
const
|
|
1401
|
-
context.stderr(`Claude exited with error: ${
|
|
1402
|
-
|
|
1403
|
-
context.stdout("✅ Claude session complete. Continuing loop...");
|
|
1404
|
-
context.stdout("");
|
|
1722
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1723
|
+
context.stderr(`Claude exited with error: ${errorMessage}`);
|
|
1724
|
+
emit({ type: "claude.ended", success: false, error: errorMessage });
|
|
1405
1725
|
return "claude_error";
|
|
1406
1726
|
}
|
|
1407
1727
|
}
|
|
@@ -1416,25 +1736,38 @@ function parseMaxIterations(commandArguments) {
|
|
|
1416
1736
|
return parsed;
|
|
1417
1737
|
}
|
|
1418
1738
|
async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
|
|
1419
|
-
const { context } = dependencies;
|
|
1739
|
+
const { context, settings } = dependencies;
|
|
1740
|
+
const { postEvent } = loopDependencies;
|
|
1420
1741
|
const maxIterations = parseMaxIterations(dependencies.arguments);
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1742
|
+
const eventsUrl = settings.eventsUrl;
|
|
1743
|
+
const sessionId = crypto.randomUUID();
|
|
1744
|
+
const postTypedEvent = createEventPoster(eventsUrl, sessionId, postEvent, (error) => {
|
|
1745
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1746
|
+
context.stderr(`Event POST failed: ${message}`);
|
|
1747
|
+
});
|
|
1748
|
+
const emit = (event) => {
|
|
1749
|
+
context.stdout(formatEvent(event));
|
|
1750
|
+
postTypedEvent(event);
|
|
1751
|
+
};
|
|
1752
|
+
emit({ type: "loop.warning" });
|
|
1753
|
+
emit({ type: "loop.started", maxIterations });
|
|
1424
1754
|
context.stdout(" Press Ctrl+C to stop");
|
|
1425
1755
|
context.stdout("");
|
|
1426
1756
|
let completedIterations = 0;
|
|
1427
1757
|
while (completedIterations < maxIterations) {
|
|
1428
|
-
const result = await runOneIteration(dependencies, loopDependencies);
|
|
1758
|
+
const result = await runOneIteration(dependencies, loopDependencies, emit);
|
|
1429
1759
|
if (result === "no_tasks") {
|
|
1430
1760
|
await loopDependencies.sleep(SLEEP_INTERVAL_MS);
|
|
1431
1761
|
} else {
|
|
1432
1762
|
completedIterations++;
|
|
1433
|
-
|
|
1434
|
-
|
|
1763
|
+
emit({
|
|
1764
|
+
type: "loop.iteration_complete",
|
|
1765
|
+
iteration: completedIterations,
|
|
1766
|
+
maxIterations
|
|
1767
|
+
});
|
|
1435
1768
|
}
|
|
1436
1769
|
}
|
|
1437
|
-
|
|
1770
|
+
emit({ type: "loop.ended", maxIterations });
|
|
1438
1771
|
return { exitCode: 0 };
|
|
1439
1772
|
}
|
|
1440
1773
|
|
|
@@ -1535,12 +1868,50 @@ async function getChangesFromRemote(cwd, gitRunner) {
|
|
|
1535
1868
|
}
|
|
1536
1869
|
return parseGitDiffNameStatus(diffResult.output);
|
|
1537
1870
|
}
|
|
1538
|
-
async function
|
|
1871
|
+
async function getUncommittedFiles(cwd, gitRunner) {
|
|
1872
|
+
const result = await gitRunner.run(["status", "--porcelain"], cwd);
|
|
1873
|
+
if (result.exitCode !== 0 || !result.output.trim()) {
|
|
1874
|
+
return [];
|
|
1875
|
+
}
|
|
1876
|
+
const files = [];
|
|
1877
|
+
const lines = result.output.split(`
|
|
1878
|
+
`).filter((line) => line.length > 0);
|
|
1879
|
+
for (const line of lines) {
|
|
1880
|
+
if (line.length > 3) {
|
|
1881
|
+
const path = line.substring(3);
|
|
1882
|
+
const arrowIndex = path.indexOf(" -> ");
|
|
1883
|
+
if (arrowIndex !== -1) {
|
|
1884
|
+
files.push(path.substring(arrowIndex + 4));
|
|
1885
|
+
} else {
|
|
1886
|
+
files.push(path);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
return files;
|
|
1891
|
+
}
|
|
1892
|
+
async function prePush(dependencies, gitRunner = defaultGitRunner, env = process.env) {
|
|
1539
1893
|
const { context } = dependencies;
|
|
1894
|
+
if (env.DUST_UNATTENDED) {
|
|
1895
|
+
const uncommittedFiles = await getUncommittedFiles(context.cwd, gitRunner);
|
|
1896
|
+
if (uncommittedFiles.length > 0) {
|
|
1897
|
+
context.stderr("");
|
|
1898
|
+
context.stderr("⚠️ Push blocked: uncommitted changes detected in unattended mode.");
|
|
1899
|
+
context.stderr("");
|
|
1900
|
+
context.stderr("You are running in unattended mode (DUST_UNATTENDED=1) and have uncommitted files:");
|
|
1901
|
+
for (const file of uncommittedFiles) {
|
|
1902
|
+
context.stderr(` → ${file}`);
|
|
1903
|
+
}
|
|
1904
|
+
context.stderr("");
|
|
1905
|
+
context.stderr("Commit or discard these changes before pushing to avoid broken builds.");
|
|
1906
|
+
context.stderr("");
|
|
1907
|
+
return { exitCode: 1 };
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1540
1910
|
const changes = await getChangesFromRemote(context.cwd, gitRunner);
|
|
1541
1911
|
if (changes.length > 0) {
|
|
1542
1912
|
const analysis = analyzeChangesForTaskOnlyPattern(changes);
|
|
1543
|
-
|
|
1913
|
+
const agent2 = detectAgent(env);
|
|
1914
|
+
if (analysis.isTaskOnly && agent2.type === "claude-code-web") {
|
|
1544
1915
|
context.stderr("");
|
|
1545
1916
|
context.stderr("⚠️ Task-only commit detected! You added a task but did not implement it.");
|
|
1546
1917
|
context.stderr("");
|
package/package.json
CHANGED