@joshski/dust 0.1.49 → 0.1.51
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 +600 -372
- package/package.json +4 -1
package/dist/dust.js
CHANGED
|
@@ -1008,7 +1008,7 @@ import { accessSync, statSync } from "node:fs";
|
|
|
1008
1008
|
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1009
1009
|
import { createServer as httpCreateServer } from "node:http";
|
|
1010
1010
|
import { homedir } from "node:os";
|
|
1011
|
-
import { join as
|
|
1011
|
+
import { join as join9 } from "node:path";
|
|
1012
1012
|
|
|
1013
1013
|
// lib/bucket/auth.ts
|
|
1014
1014
|
import { join as join4 } from "node:path";
|
|
@@ -1177,7 +1177,7 @@ function getLogLines(buffer) {
|
|
|
1177
1177
|
}
|
|
1178
1178
|
|
|
1179
1179
|
// lib/bucket/repository.ts
|
|
1180
|
-
import { dirname as dirname2, join as
|
|
1180
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
1181
1181
|
|
|
1182
1182
|
// lib/claude/spawn-claude-code.ts
|
|
1183
1183
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
@@ -1195,7 +1195,8 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
|
|
|
1195
1195
|
systemPrompt,
|
|
1196
1196
|
sessionId,
|
|
1197
1197
|
dangerouslySkipPermissions,
|
|
1198
|
-
env
|
|
1198
|
+
env,
|
|
1199
|
+
signal
|
|
1199
1200
|
} = options;
|
|
1200
1201
|
const claudeArguments = [
|
|
1201
1202
|
"-p",
|
|
@@ -1231,19 +1232,11 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
|
|
|
1231
1232
|
if (!proc.stdout) {
|
|
1232
1233
|
throw new Error("Failed to get stdout from claude process");
|
|
1233
1234
|
}
|
|
1234
|
-
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
1235
|
-
for await (const line of rl) {
|
|
1236
|
-
if (!line.trim())
|
|
1237
|
-
continue;
|
|
1238
|
-
try {
|
|
1239
|
-
yield JSON.parse(line);
|
|
1240
|
-
} catch {}
|
|
1241
|
-
}
|
|
1242
1235
|
let stderrOutput = "";
|
|
1243
1236
|
proc.stderr?.on("data", (data) => {
|
|
1244
1237
|
stderrOutput += data.toString();
|
|
1245
1238
|
});
|
|
1246
|
-
|
|
1239
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
1247
1240
|
proc.on("close", (code) => {
|
|
1248
1241
|
if (code === 0 || code === null)
|
|
1249
1242
|
resolve();
|
|
@@ -1254,6 +1247,30 @@ async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDepe
|
|
|
1254
1247
|
});
|
|
1255
1248
|
proc.on("error", reject);
|
|
1256
1249
|
});
|
|
1250
|
+
const abortHandler = () => {
|
|
1251
|
+
if (!proc.killed) {
|
|
1252
|
+
proc.kill();
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
if (signal?.aborted) {
|
|
1256
|
+
abortHandler();
|
|
1257
|
+
} else if (signal) {
|
|
1258
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1259
|
+
}
|
|
1260
|
+
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
1261
|
+
try {
|
|
1262
|
+
for await (const line of rl) {
|
|
1263
|
+
if (!line.trim())
|
|
1264
|
+
continue;
|
|
1265
|
+
try {
|
|
1266
|
+
yield JSON.parse(line);
|
|
1267
|
+
} catch {}
|
|
1268
|
+
}
|
|
1269
|
+
await closePromise;
|
|
1270
|
+
} finally {
|
|
1271
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
1272
|
+
rl.close?.();
|
|
1273
|
+
}
|
|
1257
1274
|
}
|
|
1258
1275
|
|
|
1259
1276
|
// lib/claude/event-parser.ts
|
|
@@ -1578,11 +1595,84 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
|
|
|
1578
1595
|
await dependencies.streamEvents(events, sink, onRawEvent);
|
|
1579
1596
|
}
|
|
1580
1597
|
|
|
1581
|
-
// lib/
|
|
1598
|
+
// lib/logging/match.ts
|
|
1599
|
+
function parsePatterns(debug) {
|
|
1600
|
+
if (!debug)
|
|
1601
|
+
return [];
|
|
1602
|
+
const expressions = debug.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1603
|
+
return expressions.map((expr) => {
|
|
1604
|
+
const escaped = expr.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1605
|
+
const pattern = escaped.replace(/\*/g, ".*");
|
|
1606
|
+
return new RegExp(`^${pattern}$`);
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
function matchesAny(name, patterns) {
|
|
1610
|
+
return patterns.some((re) => re.test(name));
|
|
1611
|
+
}
|
|
1612
|
+
function formatLine(name, messages) {
|
|
1613
|
+
const text = messages.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
|
|
1614
|
+
return `${new Date().toISOString()} [${name}] ${text}
|
|
1615
|
+
`;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// lib/logging/sink.ts
|
|
1619
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
1582
1620
|
import { join as join5 } from "node:path";
|
|
1621
|
+
var logPath;
|
|
1622
|
+
var ready = false;
|
|
1623
|
+
var scope = process.env.DEBUG_LOG_SCOPE || "debug";
|
|
1624
|
+
function ensureLogFile() {
|
|
1625
|
+
if (ready)
|
|
1626
|
+
return logPath;
|
|
1627
|
+
ready = true;
|
|
1628
|
+
const dir = join5(process.cwd(), "log", "dust");
|
|
1629
|
+
logPath = join5(dir, `${scope}.log`);
|
|
1630
|
+
try {
|
|
1631
|
+
mkdirSync(dir, { recursive: true });
|
|
1632
|
+
} catch {
|
|
1633
|
+
logPath = undefined;
|
|
1634
|
+
}
|
|
1635
|
+
return logPath;
|
|
1636
|
+
}
|
|
1637
|
+
function setLogScope(name) {
|
|
1638
|
+
scope = name;
|
|
1639
|
+
process.env.DEBUG_LOG_SCOPE = name;
|
|
1640
|
+
logPath = undefined;
|
|
1641
|
+
ready = false;
|
|
1642
|
+
}
|
|
1643
|
+
var writeToFile = (line) => {
|
|
1644
|
+
const path = ensureLogFile();
|
|
1645
|
+
if (!path)
|
|
1646
|
+
return;
|
|
1647
|
+
try {
|
|
1648
|
+
appendFileSync(path, line);
|
|
1649
|
+
} catch {}
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// lib/logging/index.ts
|
|
1653
|
+
var patterns = null;
|
|
1654
|
+
var initialized = false;
|
|
1655
|
+
function init() {
|
|
1656
|
+
if (initialized)
|
|
1657
|
+
return;
|
|
1658
|
+
initialized = true;
|
|
1659
|
+
const parsed = parsePatterns(process.env.DEBUG);
|
|
1660
|
+
patterns = parsed.length > 0 ? parsed : null;
|
|
1661
|
+
}
|
|
1662
|
+
function createLogger(name, write = writeToFile) {
|
|
1663
|
+
return (...messages) => {
|
|
1664
|
+
init();
|
|
1665
|
+
if (!patterns || !matchesAny(name, patterns))
|
|
1666
|
+
return;
|
|
1667
|
+
write(formatLine(name, messages));
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// lib/bucket/repository-git.ts
|
|
1672
|
+
import { join as join6 } from "node:path";
|
|
1583
1673
|
function getRepoPath(repoName, reposDir) {
|
|
1584
1674
|
const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
|
|
1585
|
-
return
|
|
1675
|
+
return join6(reposDir, safeName);
|
|
1586
1676
|
}
|
|
1587
1677
|
async function cloneRepository(repository, targetPath, spawn, context) {
|
|
1588
1678
|
return new Promise((resolve) => {
|
|
@@ -1652,7 +1742,7 @@ function formatAgentEvent(event) {
|
|
|
1652
1742
|
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
1653
1743
|
import { readFileSync } from "node:fs";
|
|
1654
1744
|
import os from "node:os";
|
|
1655
|
-
import { dirname, join as
|
|
1745
|
+
import { dirname, join as join7 } from "node:path";
|
|
1656
1746
|
import { fileURLToPath } from "node:url";
|
|
1657
1747
|
|
|
1658
1748
|
// lib/workflow-tasks.ts
|
|
@@ -1800,8 +1890,8 @@ async function next(dependencies) {
|
|
|
1800
1890
|
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
1801
1891
|
function getDustVersion() {
|
|
1802
1892
|
const candidates = [
|
|
1803
|
-
|
|
1804
|
-
|
|
1893
|
+
join7(__dirname2, "../../../package.json"),
|
|
1894
|
+
join7(__dirname2, "../package.json")
|
|
1805
1895
|
];
|
|
1806
1896
|
for (const candidate of candidates) {
|
|
1807
1897
|
try {
|
|
@@ -1881,6 +1971,7 @@ function createWireEventSender(eventsUrl, sessionId, postEvent, onError, getAgen
|
|
|
1881
1971
|
postEvent(eventsUrl, payload).catch(onError);
|
|
1882
1972
|
};
|
|
1883
1973
|
}
|
|
1974
|
+
var log = createLogger("dust.cli.commands.loop");
|
|
1884
1975
|
var SLEEP_INTERVAL_MS = 30000;
|
|
1885
1976
|
var DEFAULT_MAX_ITERATIONS = 10;
|
|
1886
1977
|
async function gitPull(cwd, spawn) {
|
|
@@ -1914,10 +2005,12 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
|
|
|
1914
2005
|
const { context } = dependencies;
|
|
1915
2006
|
const { spawn, run: run2 } = loopDependencies;
|
|
1916
2007
|
const agentName = loopDependencies.agentType === "codex" ? "Codex" : "Claude";
|
|
1917
|
-
const { onRawEvent, hooksInstalled = false } = options;
|
|
2008
|
+
const { onRawEvent, hooksInstalled = false, signal } = options;
|
|
2009
|
+
log("syncing with remote");
|
|
1918
2010
|
onLoopEvent({ type: "loop.syncing" });
|
|
1919
2011
|
const pullResult = await gitPull(context.cwd, spawn);
|
|
1920
2012
|
if (!pullResult.success) {
|
|
2013
|
+
log(`git pull failed: ${pullResult.message}`);
|
|
1921
2014
|
onLoopEvent({
|
|
1922
2015
|
type: "loop.sync_skipped",
|
|
1923
2016
|
reason: pullResult.message
|
|
@@ -1947,7 +2040,8 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1947
2040
|
spawnOptions: {
|
|
1948
2041
|
cwd: context.cwd,
|
|
1949
2042
|
dangerouslySkipPermissions: true,
|
|
1950
|
-
env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
|
|
2043
|
+
env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" },
|
|
2044
|
+
signal
|
|
1951
2045
|
},
|
|
1952
2046
|
onRawEvent
|
|
1953
2047
|
});
|
|
@@ -1966,10 +2060,12 @@ Make sure the repository is in a clean state and synced with remote before finis
|
|
|
1966
2060
|
onLoopEvent({ type: "loop.checking_tasks" });
|
|
1967
2061
|
const tasks = await findAvailableTasks(dependencies);
|
|
1968
2062
|
if (tasks.length === 0) {
|
|
2063
|
+
log("no tasks available");
|
|
1969
2064
|
onLoopEvent({ type: "loop.no_tasks" });
|
|
1970
2065
|
return "no_tasks";
|
|
1971
2066
|
}
|
|
1972
2067
|
const task = tasks[0];
|
|
2068
|
+
log(`found ${tasks.length} task(s), picking: ${task.title ?? task.path}`);
|
|
1973
2069
|
onLoopEvent({ type: "loop.tasks_found" });
|
|
1974
2070
|
const taskContent = await dependencies.fileSystem.readFile(`${dependencies.context.cwd}/${task.path}`);
|
|
1975
2071
|
const { dustCommand, installCommand = "npm install" } = dependencies.settings;
|
|
@@ -2000,14 +2096,17 @@ ${instructions}`;
|
|
|
2000
2096
|
spawnOptions: {
|
|
2001
2097
|
cwd: context.cwd,
|
|
2002
2098
|
dangerouslySkipPermissions: true,
|
|
2003
|
-
env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" }
|
|
2099
|
+
env: { DUST_UNATTENDED: "1", DUST_SKIP_AGENT: "1" },
|
|
2100
|
+
signal
|
|
2004
2101
|
},
|
|
2005
2102
|
onRawEvent
|
|
2006
2103
|
});
|
|
2104
|
+
log(`${agentName} completed task: ${task.title ?? task.path}`);
|
|
2007
2105
|
onAgentEvent?.({ type: "agent-session-ended", success: true });
|
|
2008
2106
|
return "ran_claude";
|
|
2009
2107
|
} catch (error) {
|
|
2010
2108
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2109
|
+
log(`${agentName} error on task ${task.title ?? task.path}: ${errorMessage}`);
|
|
2011
2110
|
context.stderr(`${agentName} exited with error: ${errorMessage}`);
|
|
2012
2111
|
onAgentEvent?.({
|
|
2013
2112
|
type: "agent-session-ended",
|
|
@@ -2052,6 +2151,7 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
2052
2151
|
sendWireEvent(event);
|
|
2053
2152
|
};
|
|
2054
2153
|
const hooksInstalled = await manageGitHooks(dependencies);
|
|
2154
|
+
log(`starting loop, maxIterations=${maxIterations}, sessionId=${sessionId}`);
|
|
2055
2155
|
onLoopEvent({ type: "loop.warning" });
|
|
2056
2156
|
onLoopEvent({
|
|
2057
2157
|
type: "loop.started",
|
|
@@ -2071,9 +2171,11 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
2071
2171
|
agentSessionId = crypto.randomUUID();
|
|
2072
2172
|
const result = await runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, iterationOptions);
|
|
2073
2173
|
if (result === "no_tasks") {
|
|
2174
|
+
log("sleeping, no tasks");
|
|
2074
2175
|
await loopDependencies.sleep(SLEEP_INTERVAL_MS);
|
|
2075
2176
|
} else {
|
|
2076
2177
|
completedIterations++;
|
|
2178
|
+
log(`iteration ${completedIterations}/${maxIterations} complete, result=${result}`);
|
|
2077
2179
|
onLoopEvent({
|
|
2078
2180
|
type: "loop.iteration_complete",
|
|
2079
2181
|
iteration: completedIterations,
|
|
@@ -2081,11 +2183,13 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
|
|
|
2081
2183
|
});
|
|
2082
2184
|
}
|
|
2083
2185
|
}
|
|
2186
|
+
log(`loop ended after ${completedIterations} iterations`);
|
|
2084
2187
|
onLoopEvent({ type: "loop.ended", maxIterations });
|
|
2085
2188
|
return { exitCode: 0 };
|
|
2086
2189
|
}
|
|
2087
2190
|
|
|
2088
2191
|
// lib/bucket/repository-loop.ts
|
|
2192
|
+
var log2 = createLogger("dust.bucket.repository-loop");
|
|
2089
2193
|
var FALLBACK_TIMEOUT_MS = 300000;
|
|
2090
2194
|
function createNoOpGlobScanner() {
|
|
2091
2195
|
return {
|
|
@@ -2173,26 +2277,55 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
|
|
|
2173
2277
|
}
|
|
2174
2278
|
};
|
|
2175
2279
|
const hooksInstalled = await manageGitHooks(commandDeps);
|
|
2280
|
+
const logLine = (msg) => appendLogLine(repoState.logBuffer, createLogLine(msg, "stdout"));
|
|
2281
|
+
log2(`loop started for ${repoName} at ${repoState.path}`);
|
|
2176
2282
|
while (!repoState.stopRequested) {
|
|
2177
2283
|
agentSessionId = crypto.randomUUID();
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2284
|
+
const abortController = new AbortController;
|
|
2285
|
+
const cancelCurrentIteration = () => {
|
|
2286
|
+
abortController.abort();
|
|
2287
|
+
};
|
|
2288
|
+
repoState.cancelCurrentIteration = cancelCurrentIteration;
|
|
2289
|
+
let result;
|
|
2290
|
+
try {
|
|
2291
|
+
result = await runOneIteration(commandDeps, loopDeps, onLoopEvent, onAgentEvent, {
|
|
2292
|
+
hooksInstalled,
|
|
2293
|
+
signal: abortController.signal,
|
|
2294
|
+
onRawEvent: (rawEvent) => {
|
|
2295
|
+
onAgentEvent(rawEventToAgentEvent(rawEvent));
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2300
|
+
log2(`iteration error for ${repoName}: ${msg}`);
|
|
2301
|
+
appendLogLine(repoState.logBuffer, createLogLine(`Loop error: ${msg}`, "stderr"));
|
|
2302
|
+
await sleep(1e4);
|
|
2303
|
+
continue;
|
|
2304
|
+
} finally {
|
|
2305
|
+
if (repoState.cancelCurrentIteration === cancelCurrentIteration) {
|
|
2306
|
+
repoState.cancelCurrentIteration = undefined;
|
|
2182
2307
|
}
|
|
2183
|
-
}
|
|
2308
|
+
}
|
|
2184
2309
|
if (result === "no_tasks") {
|
|
2185
2310
|
if (repoState.taskAvailablePending) {
|
|
2186
2311
|
repoState.taskAvailablePending = false;
|
|
2312
|
+
log2(`${repoName}: task signal received during iteration, rechecking`);
|
|
2313
|
+
logLine("Task signal received during iteration, rechecking...");
|
|
2187
2314
|
continue;
|
|
2188
2315
|
}
|
|
2316
|
+
log2(`${repoName}: no tasks available, waiting`);
|
|
2317
|
+
logLine("Waiting for tasks...");
|
|
2189
2318
|
await new Promise((resolve) => {
|
|
2190
|
-
|
|
2319
|
+
const wakeUpForThisWait = () => {
|
|
2320
|
+
if (repoState.wakeUp !== wakeUpForThisWait) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2191
2323
|
repoState.wakeUp = undefined;
|
|
2192
2324
|
resolve();
|
|
2193
2325
|
};
|
|
2326
|
+
repoState.wakeUp = wakeUpForThisWait;
|
|
2194
2327
|
sleep(FALLBACK_TIMEOUT_MS).then(() => {
|
|
2195
|
-
if (repoState.wakeUp) {
|
|
2328
|
+
if (repoState.wakeUp === wakeUpForThisWait) {
|
|
2196
2329
|
repoState.wakeUp = undefined;
|
|
2197
2330
|
resolve();
|
|
2198
2331
|
}
|
|
@@ -2200,9 +2333,27 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
|
|
|
2200
2333
|
});
|
|
2201
2334
|
}
|
|
2202
2335
|
}
|
|
2336
|
+
log2(`loop stopped for ${repoName}`);
|
|
2203
2337
|
appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
|
|
2204
2338
|
}
|
|
2339
|
+
|
|
2205
2340
|
// lib/bucket/repository.ts
|
|
2341
|
+
var log3 = createLogger("dust.bucket.repository");
|
|
2342
|
+
function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
|
|
2343
|
+
log3(`starting loop for ${repoState.repository.name}`);
|
|
2344
|
+
repoState.stopRequested = false;
|
|
2345
|
+
repoState.loopPromise = runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId).catch((error) => {
|
|
2346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2347
|
+
log3(`loop crashed for ${repoState.repository.name}: ${message}`);
|
|
2348
|
+
appendLogLine(repoState.logBuffer, createLogLine(`Repository loop crashed: ${message}`, "stderr"));
|
|
2349
|
+
}).finally(() => {
|
|
2350
|
+
log3(`loop finished for ${repoState.repository.name}`);
|
|
2351
|
+
repoState.loopPromise = null;
|
|
2352
|
+
repoState.agentStatus = "idle";
|
|
2353
|
+
repoState.wakeUp = undefined;
|
|
2354
|
+
repoState.cancelCurrentIteration = undefined;
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2206
2357
|
function parseRepository(data) {
|
|
2207
2358
|
if (typeof data === "string") {
|
|
2208
2359
|
return { name: data, gitUrl: data };
|
|
@@ -2224,8 +2375,10 @@ function parseRepository(data) {
|
|
|
2224
2375
|
}
|
|
2225
2376
|
async function addRepository(repository, manager, repoDeps, context) {
|
|
2226
2377
|
if (manager.repositories.has(repository.name)) {
|
|
2378
|
+
log3(`repository ${repository.name} already exists, skipping add`);
|
|
2227
2379
|
return;
|
|
2228
2380
|
}
|
|
2381
|
+
log3(`adding repository ${repository.name}`);
|
|
2229
2382
|
const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
|
|
2230
2383
|
await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
|
|
2231
2384
|
if (repoDeps.fileSystem.exists(repoPath)) {
|
|
@@ -2258,14 +2411,16 @@ async function addRepository(repository, manager, repoDeps, context) {
|
|
|
2258
2411
|
};
|
|
2259
2412
|
manager.emit(addedEvent);
|
|
2260
2413
|
context.stdout(formatBucketEvent(addedEvent));
|
|
2261
|
-
|
|
2414
|
+
startRepositoryLoop(repoState, repoDeps, manager.sendEvent, manager.sessionId);
|
|
2262
2415
|
}
|
|
2263
2416
|
async function removeRepositoryFromManager(repoName, manager, repoDeps, context) {
|
|
2264
2417
|
const repoState = manager.repositories.get(repoName);
|
|
2265
2418
|
if (!repoState) {
|
|
2266
2419
|
return;
|
|
2267
2420
|
}
|
|
2421
|
+
log3(`removing repository ${repoName}`);
|
|
2268
2422
|
repoState.stopRequested = true;
|
|
2423
|
+
repoState.cancelCurrentIteration?.();
|
|
2269
2424
|
repoState.wakeUp?.();
|
|
2270
2425
|
if (repoState.loopPromise) {
|
|
2271
2426
|
await Promise.race([repoState.loopPromise, repoDeps.sleep(5000)]);
|
|
@@ -2576,14 +2731,24 @@ function formatLogLine(line, prefixAlign, maxWidth) {
|
|
|
2576
2731
|
prefix = `${line.color}${paddedName}${ANSI.RESET} ${ANSI.DIM}|${ANSI.RESET} `;
|
|
2577
2732
|
prefixWidth = prefixAlign + 3;
|
|
2578
2733
|
}
|
|
2734
|
+
let timePrefix = "";
|
|
2735
|
+
let timePrefixWidth = 0;
|
|
2736
|
+
if (line.repository === "system") {
|
|
2737
|
+
const d = new Date(line.timestamp);
|
|
2738
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
2739
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
2740
|
+
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
2741
|
+
timePrefix = `${ANSI.DIM}${hh}:${mm}:${ss}${ANSI.RESET} `;
|
|
2742
|
+
timePrefixWidth = 9;
|
|
2743
|
+
}
|
|
2579
2744
|
const textColor = line.stream === "stderr" ? ANSI.FG_RED : "";
|
|
2580
2745
|
const textReset = line.stream === "stderr" ? ANSI.RESET : "";
|
|
2581
2746
|
const sanitizedText = line.text.replace(/[\r\n]+/g, "");
|
|
2582
2747
|
const text = `${textColor}${sanitizedText}${textReset}`;
|
|
2583
|
-
const availableWidth = maxWidth - prefixWidth;
|
|
2748
|
+
const availableWidth = maxWidth - prefixWidth - timePrefixWidth;
|
|
2584
2749
|
if (availableWidth <= 0)
|
|
2585
|
-
return prefix;
|
|
2586
|
-
return prefix + truncateLine(text, availableWidth);
|
|
2750
|
+
return prefix + timePrefix;
|
|
2751
|
+
return prefix + timePrefix + truncateLine(text, availableWidth);
|
|
2587
2752
|
}
|
|
2588
2753
|
function renderFrame(state) {
|
|
2589
2754
|
const lines = [];
|
|
@@ -2708,6 +2873,7 @@ function handleKeyInput(state, key, options) {
|
|
|
2708
2873
|
}
|
|
2709
2874
|
|
|
2710
2875
|
// lib/cli/commands/bucket.ts
|
|
2876
|
+
var log4 = createLogger("dust.cli.commands.bucket");
|
|
2711
2877
|
var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
|
|
2712
2878
|
var INITIAL_RECONNECT_DELAY_MS = 1000;
|
|
2713
2879
|
var MAX_RECONNECT_DELAY_MS = 30000;
|
|
@@ -2829,7 +2995,7 @@ function createDefaultBucketDependencies() {
|
|
|
2829
2995
|
writeStdout: defaultWriteStdout,
|
|
2830
2996
|
isTTY: process.stdout.isTTY ?? false,
|
|
2831
2997
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
2832
|
-
getReposDir: () => process.env.DUST_REPOS_DIR ||
|
|
2998
|
+
getReposDir: () => process.env.DUST_REPOS_DIR || join9(homedir(), ".dust", "repos"),
|
|
2833
2999
|
auth: {
|
|
2834
3000
|
createServer: defaultCreateServer,
|
|
2835
3001
|
openBrowser: defaultOpenBrowser,
|
|
@@ -2870,6 +3036,25 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
|
|
|
2870
3036
|
getReposDir: bucketDeps.getReposDir
|
|
2871
3037
|
};
|
|
2872
3038
|
}
|
|
3039
|
+
function ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI) {
|
|
3040
|
+
if (repoState.loopPromise || repoState.wakeUp || repoState.stopRequested) {
|
|
3041
|
+
log4(`loop already running/waiting for ${repoState.repository.name}`);
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
logMessage(state, context, useTUI, `Repository loop not running for ${repoState.repository.name}; restarting`);
|
|
3045
|
+
startRepositoryLoop(repoState, repoDeps, state.sendEvent, state.sessionId);
|
|
3046
|
+
}
|
|
3047
|
+
function signalTaskAvailable(repoState, state, repoDeps, context, useTUI) {
|
|
3048
|
+
log4(`task-available signal for ${repoState.repository.name}`);
|
|
3049
|
+
ensureRepositoryLoopRunning(repoState, state, repoDeps, context, useTUI);
|
|
3050
|
+
if (repoState.wakeUp) {
|
|
3051
|
+
log4(`waking loop for ${repoState.repository.name}`);
|
|
3052
|
+
repoState.wakeUp();
|
|
3053
|
+
} else {
|
|
3054
|
+
log4(`marking task pending for ${repoState.repository.name} (loop busy)`);
|
|
3055
|
+
repoState.taskAvailablePending = true;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
2873
3058
|
function syncUIWithRepoList(state, repos) {
|
|
2874
3059
|
const incomingNames = new Set;
|
|
2875
3060
|
for (const data of repos) {
|
|
@@ -2993,9 +3178,14 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
|
|
|
2993
3178
|
ws.onmessage = (event) => {
|
|
2994
3179
|
try {
|
|
2995
3180
|
const message = JSON.parse(event.data);
|
|
3181
|
+
log4(`ws message: ${message.type}`);
|
|
2996
3182
|
if (message.type === "repository-list") {
|
|
2997
3183
|
const repos = message.repositories ?? [];
|
|
2998
|
-
|
|
3184
|
+
const repoNames = repos.map((r) => {
|
|
3185
|
+
const name = r?.name ?? "?";
|
|
3186
|
+
return r?.hasTask ? `${name} (has task)` : name;
|
|
3187
|
+
}).join(", ");
|
|
3188
|
+
logMessage(state, context, useTUI, `Received repository list: ${repoNames || "(empty)"}`);
|
|
2999
3189
|
syncUIWithRepoList(state, repos);
|
|
3000
3190
|
const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
|
|
3001
3191
|
const repoContext = createTUIContext(state, context, useTUI);
|
|
@@ -3005,11 +3195,7 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
|
|
|
3005
3195
|
if (typeof repoData === "object" && repoData !== null && "name" in repoData && "hasTask" in repoData && repoData.hasTask) {
|
|
3006
3196
|
const repoState = state.repositories.get(repoData.name);
|
|
3007
3197
|
if (repoState) {
|
|
3008
|
-
|
|
3009
|
-
repoState.wakeUp();
|
|
3010
|
-
} else {
|
|
3011
|
-
repoState.taskAvailablePending = true;
|
|
3012
|
-
}
|
|
3198
|
+
signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
|
|
3013
3199
|
}
|
|
3014
3200
|
}
|
|
3015
3201
|
}
|
|
@@ -3019,13 +3205,13 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
|
|
|
3019
3205
|
} else if (message.type === "task-available") {
|
|
3020
3206
|
const repoName = message.repository;
|
|
3021
3207
|
if (typeof repoName === "string") {
|
|
3208
|
+
const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
|
|
3209
|
+
logMessage(state, context, useTUI, `Received task-available for ${repoName}`);
|
|
3022
3210
|
const repoState = state.repositories.get(repoName);
|
|
3023
3211
|
if (repoState) {
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
repoState.taskAvailablePending = true;
|
|
3028
|
-
}
|
|
3212
|
+
signalTaskAvailable(repoState, state, repoDeps, context, useTUI);
|
|
3213
|
+
} else {
|
|
3214
|
+
logMessage(state, context, useTUI, `No repository state found for ${repoName}`, "stderr");
|
|
3029
3215
|
}
|
|
3030
3216
|
}
|
|
3031
3217
|
}
|
|
@@ -3038,6 +3224,7 @@ async function shutdown(state, bucketDeps, context) {
|
|
|
3038
3224
|
if (state.shuttingDown)
|
|
3039
3225
|
return;
|
|
3040
3226
|
state.shuttingDown = true;
|
|
3227
|
+
log4("shutdown initiated");
|
|
3041
3228
|
context.stdout("Shutting down...");
|
|
3042
3229
|
if (state.reconnectTimer) {
|
|
3043
3230
|
clearTimeout(state.reconnectTimer);
|
|
@@ -3049,6 +3236,7 @@ async function shutdown(state, bucketDeps, context) {
|
|
|
3049
3236
|
}
|
|
3050
3237
|
for (const repoState of state.repositories.values()) {
|
|
3051
3238
|
repoState.stopRequested = true;
|
|
3239
|
+
repoState.cancelCurrentIteration?.();
|
|
3052
3240
|
repoState.wakeUp?.();
|
|
3053
3241
|
}
|
|
3054
3242
|
const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
|
|
@@ -3229,39 +3417,25 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
|
|
|
3229
3417
|
}
|
|
3230
3418
|
|
|
3231
3419
|
// lib/cli/commands/lint-markdown.ts
|
|
3232
|
-
import {
|
|
3420
|
+
import { join as join10 } from "node:path";
|
|
3421
|
+
|
|
3422
|
+
// lib/lint/validators/content-validator.ts
|
|
3233
3423
|
var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
|
|
3234
|
-
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
3235
|
-
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
3236
|
-
var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
3237
3424
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
return null;
|
|
3253
|
-
}
|
|
3254
|
-
const parts = filePath.split("/");
|
|
3255
|
-
const actualFilename = parts[parts.length - 1];
|
|
3256
|
-
const expectedFilename = titleToFilename(title);
|
|
3257
|
-
if (actualFilename !== expectedFilename) {
|
|
3258
|
-
return {
|
|
3259
|
-
file: filePath,
|
|
3260
|
-
message: `Filename "${actualFilename}" does not match title "${title}" (expected "${expectedFilename}")`
|
|
3261
|
-
};
|
|
3262
|
-
}
|
|
3263
|
-
return null;
|
|
3264
|
-
}
|
|
3425
|
+
var NON_IMPERATIVE_STARTERS = new Set([
|
|
3426
|
+
"the",
|
|
3427
|
+
"a",
|
|
3428
|
+
"an",
|
|
3429
|
+
"this",
|
|
3430
|
+
"that",
|
|
3431
|
+
"these",
|
|
3432
|
+
"those",
|
|
3433
|
+
"we",
|
|
3434
|
+
"it",
|
|
3435
|
+
"they",
|
|
3436
|
+
"you",
|
|
3437
|
+
"i"
|
|
3438
|
+
]);
|
|
3265
3439
|
function validateOpeningSentence(filePath, content) {
|
|
3266
3440
|
const openingSentence = extractOpeningSentence(content);
|
|
3267
3441
|
if (!openingSentence) {
|
|
@@ -3285,20 +3459,6 @@ function validateOpeningSentenceLength(filePath, content) {
|
|
|
3285
3459
|
}
|
|
3286
3460
|
return null;
|
|
3287
3461
|
}
|
|
3288
|
-
var NON_IMPERATIVE_STARTERS = new Set([
|
|
3289
|
-
"the",
|
|
3290
|
-
"a",
|
|
3291
|
-
"an",
|
|
3292
|
-
"this",
|
|
3293
|
-
"that",
|
|
3294
|
-
"these",
|
|
3295
|
-
"those",
|
|
3296
|
-
"we",
|
|
3297
|
-
"it",
|
|
3298
|
-
"they",
|
|
3299
|
-
"you",
|
|
3300
|
-
"i"
|
|
3301
|
-
]);
|
|
3302
3462
|
function validateImperativeOpeningSentence(filePath, content) {
|
|
3303
3463
|
const openingSentence = extractOpeningSentence(content);
|
|
3304
3464
|
if (!openingSentence) {
|
|
@@ -3327,194 +3487,109 @@ function validateTaskHeadings(filePath, content) {
|
|
|
3327
3487
|
}
|
|
3328
3488
|
return violations;
|
|
3329
3489
|
}
|
|
3330
|
-
|
|
3490
|
+
|
|
3491
|
+
// lib/lint/validators/directory-validator.ts
|
|
3492
|
+
var EXPECTED_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
3493
|
+
async function validateContentDirectoryFiles(dirPath, fileSystem) {
|
|
3331
3494
|
const violations = [];
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
let match = linkPattern.exec(line);
|
|
3339
|
-
while (match) {
|
|
3340
|
-
const linkTarget = match[2];
|
|
3341
|
-
if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
|
|
3342
|
-
const targetPath = linkTarget.split("#")[0];
|
|
3343
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
3344
|
-
if (!fileSystem.exists(resolvedPath)) {
|
|
3345
|
-
violations.push({
|
|
3346
|
-
file: filePath,
|
|
3347
|
-
message: `Broken link: "${linkTarget}"`,
|
|
3348
|
-
line: i + 1
|
|
3349
|
-
});
|
|
3350
|
-
}
|
|
3351
|
-
}
|
|
3352
|
-
match = linkPattern.exec(line);
|
|
3495
|
+
let entries;
|
|
3496
|
+
try {
|
|
3497
|
+
entries = await fileSystem.readdir(dirPath);
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
if (error.code === "ENOENT") {
|
|
3500
|
+
return [];
|
|
3353
3501
|
}
|
|
3502
|
+
throw error;
|
|
3354
3503
|
}
|
|
3355
|
-
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
|
-
const violations = [];
|
|
3359
|
-
const lines = content.split(`
|
|
3360
|
-
`);
|
|
3361
|
-
let inOpenQuestions = false;
|
|
3362
|
-
let currentQuestionLine = null;
|
|
3363
|
-
let inCodeBlock = false;
|
|
3364
|
-
for (let i = 0;i < lines.length; i++) {
|
|
3365
|
-
const line = lines[i];
|
|
3366
|
-
if (line.startsWith("```")) {
|
|
3367
|
-
inCodeBlock = !inCodeBlock;
|
|
3368
|
-
continue;
|
|
3369
|
-
}
|
|
3370
|
-
if (inCodeBlock)
|
|
3371
|
-
continue;
|
|
3372
|
-
if (line.startsWith("## ")) {
|
|
3373
|
-
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
3374
|
-
violations.push({
|
|
3375
|
-
file: filePath,
|
|
3376
|
-
message: "Question has no options listed beneath it",
|
|
3377
|
-
line: currentQuestionLine
|
|
3378
|
-
});
|
|
3379
|
-
}
|
|
3380
|
-
const headingText = line.slice(3).trimEnd();
|
|
3381
|
-
if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
|
|
3382
|
-
violations.push({
|
|
3383
|
-
file: filePath,
|
|
3384
|
-
message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
|
|
3385
|
-
line: i + 1
|
|
3386
|
-
});
|
|
3387
|
-
}
|
|
3388
|
-
inOpenQuestions = line === "## Open Questions";
|
|
3389
|
-
currentQuestionLine = null;
|
|
3390
|
-
continue;
|
|
3391
|
-
}
|
|
3392
|
-
if (!inOpenQuestions)
|
|
3393
|
-
continue;
|
|
3394
|
-
if (/^[-*] /.test(line.trimStart())) {
|
|
3504
|
+
for (const entry of entries) {
|
|
3505
|
+
const entryPath = `${dirPath}/${entry}`;
|
|
3506
|
+
if (entry.startsWith(".")) {
|
|
3395
3507
|
violations.push({
|
|
3396
|
-
file:
|
|
3397
|
-
message:
|
|
3398
|
-
line: i + 1
|
|
3508
|
+
file: entryPath,
|
|
3509
|
+
message: `Hidden file "${entry}" found in content directory`
|
|
3399
3510
|
});
|
|
3400
3511
|
continue;
|
|
3401
3512
|
}
|
|
3402
|
-
if (
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
line: currentQuestionLine
|
|
3408
|
-
});
|
|
3409
|
-
}
|
|
3410
|
-
if (!line.trimEnd().endsWith("?")) {
|
|
3411
|
-
violations.push({
|
|
3412
|
-
file: filePath,
|
|
3413
|
-
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
3414
|
-
line: i + 1
|
|
3415
|
-
});
|
|
3416
|
-
currentQuestionLine = null;
|
|
3417
|
-
} else {
|
|
3418
|
-
currentQuestionLine = i + 1;
|
|
3419
|
-
}
|
|
3513
|
+
if (fileSystem.isDirectory(entryPath)) {
|
|
3514
|
+
violations.push({
|
|
3515
|
+
file: entryPath,
|
|
3516
|
+
message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
|
|
3517
|
+
});
|
|
3420
3518
|
continue;
|
|
3421
3519
|
}
|
|
3422
|
-
if (
|
|
3423
|
-
|
|
3520
|
+
if (!entry.endsWith(".md")) {
|
|
3521
|
+
violations.push({
|
|
3522
|
+
file: entryPath,
|
|
3523
|
+
message: `Non-markdown file "${entry}" found in content directory`
|
|
3524
|
+
});
|
|
3424
3525
|
}
|
|
3425
3526
|
}
|
|
3426
|
-
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
3427
|
-
violations.push({
|
|
3428
|
-
file: filePath,
|
|
3429
|
-
message: "Question has no options listed beneath it",
|
|
3430
|
-
line: currentQuestionLine
|
|
3431
|
-
});
|
|
3432
|
-
}
|
|
3433
3527
|
return violations;
|
|
3434
3528
|
}
|
|
3435
|
-
|
|
3436
|
-
{
|
|
3437
|
-
section: "## Goals",
|
|
3438
|
-
requiredPath: "/.dust/goals/",
|
|
3439
|
-
description: "goal"
|
|
3440
|
-
},
|
|
3441
|
-
{
|
|
3442
|
-
section: "## Blocked By",
|
|
3443
|
-
requiredPath: "/.dust/tasks/",
|
|
3444
|
-
description: "task"
|
|
3445
|
-
}
|
|
3446
|
-
];
|
|
3447
|
-
function validateSemanticLinks(filePath, content) {
|
|
3529
|
+
async function validateDirectoryStructure(dustPath, fileSystem, extraDirectories = []) {
|
|
3448
3530
|
const violations = [];
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
if (line.startsWith("## ")) {
|
|
3456
|
-
currentSection = line;
|
|
3457
|
-
continue;
|
|
3531
|
+
let entries;
|
|
3532
|
+
try {
|
|
3533
|
+
entries = await fileSystem.readdir(dustPath);
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
if (error.code === "ENOENT") {
|
|
3536
|
+
return [];
|
|
3458
3537
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3538
|
+
throw error;
|
|
3539
|
+
}
|
|
3540
|
+
const allowedDirectories = new Set([
|
|
3541
|
+
...EXPECTED_DIRECTORIES,
|
|
3542
|
+
...extraDirectories
|
|
3543
|
+
]);
|
|
3544
|
+
for (const entry of entries) {
|
|
3545
|
+
const entryPath = `${dustPath}/${entry}`;
|
|
3546
|
+
if (!fileSystem.isDirectory(entryPath)) {
|
|
3461
3547
|
continue;
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an anchor: "${linkTarget}"`,
|
|
3470
|
-
line: i + 1
|
|
3471
|
-
});
|
|
3472
|
-
match = linkPattern.exec(line);
|
|
3473
|
-
continue;
|
|
3474
|
-
}
|
|
3475
|
-
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
3476
|
-
violations.push({
|
|
3477
|
-
file: filePath,
|
|
3478
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an external URL: "${linkTarget}"`,
|
|
3479
|
-
line: i + 1
|
|
3480
|
-
});
|
|
3481
|
-
match = linkPattern.exec(line);
|
|
3482
|
-
continue;
|
|
3483
|
-
}
|
|
3484
|
-
const targetPath = linkTarget.split("#")[0];
|
|
3485
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
3486
|
-
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
3487
|
-
violations.push({
|
|
3488
|
-
file: filePath,
|
|
3489
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file: "${linkTarget}"`,
|
|
3490
|
-
line: i + 1
|
|
3491
|
-
});
|
|
3492
|
-
}
|
|
3493
|
-
match = linkPattern.exec(line);
|
|
3548
|
+
}
|
|
3549
|
+
if (!allowedDirectories.has(entry)) {
|
|
3550
|
+
const allowedList = [...allowedDirectories].sort().join(", ");
|
|
3551
|
+
violations.push({
|
|
3552
|
+
file: entryPath,
|
|
3553
|
+
message: `Unexpected directory "${entry}" in .dust/. Allowed directories: ${allowedList}. To allow this directory, add it to "extraDirectories" in .dust/config/settings.json`
|
|
3554
|
+
});
|
|
3494
3555
|
}
|
|
3495
3556
|
}
|
|
3496
3557
|
return violations;
|
|
3497
3558
|
}
|
|
3498
|
-
|
|
3559
|
+
|
|
3560
|
+
// lib/lint/validators/filename-validator.ts
|
|
3561
|
+
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
3562
|
+
function validateFilename(filePath) {
|
|
3563
|
+
const parts = filePath.split("/");
|
|
3564
|
+
const filename = parts[parts.length - 1];
|
|
3565
|
+
if (!SLUG_PATTERN.test(filename)) {
|
|
3566
|
+
return {
|
|
3567
|
+
file: filePath,
|
|
3568
|
+
message: `Filename "${filename}" does not match slug-style naming`
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
return null;
|
|
3572
|
+
}
|
|
3573
|
+
function validateTitleFilenameMatch(filePath, content) {
|
|
3499
3574
|
const title = extractTitle(content);
|
|
3500
3575
|
if (!title) {
|
|
3501
3576
|
return null;
|
|
3502
3577
|
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
};
|
|
3512
|
-
}
|
|
3513
|
-
return null;
|
|
3514
|
-
}
|
|
3578
|
+
const parts = filePath.split("/");
|
|
3579
|
+
const actualFilename = parts[parts.length - 1];
|
|
3580
|
+
const expectedFilename = titleToFilename(title);
|
|
3581
|
+
if (actualFilename !== expectedFilename) {
|
|
3582
|
+
return {
|
|
3583
|
+
file: filePath,
|
|
3584
|
+
message: `Filename "${actualFilename}" does not match title "${title}" (expected "${expectedFilename}")`
|
|
3585
|
+
};
|
|
3515
3586
|
}
|
|
3516
3587
|
return null;
|
|
3517
3588
|
}
|
|
3589
|
+
|
|
3590
|
+
// lib/lint/validators/goal-hierarchy.ts
|
|
3591
|
+
import { dirname as dirname3, resolve } from "node:path";
|
|
3592
|
+
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
3518
3593
|
function validateGoalHierarchySections(filePath, content) {
|
|
3519
3594
|
const violations = [];
|
|
3520
3595
|
for (const heading of REQUIRED_GOAL_HEADINGS) {
|
|
@@ -3527,57 +3602,6 @@ function validateGoalHierarchySections(filePath, content) {
|
|
|
3527
3602
|
}
|
|
3528
3603
|
return violations;
|
|
3529
3604
|
}
|
|
3530
|
-
function validateGoalHierarchyLinks(filePath, content) {
|
|
3531
|
-
const violations = [];
|
|
3532
|
-
const lines = content.split(`
|
|
3533
|
-
`);
|
|
3534
|
-
const fileDir = dirname3(filePath);
|
|
3535
|
-
let currentSection = null;
|
|
3536
|
-
for (let i = 0;i < lines.length; i++) {
|
|
3537
|
-
const line = lines[i];
|
|
3538
|
-
if (line.startsWith("## ")) {
|
|
3539
|
-
currentSection = line;
|
|
3540
|
-
continue;
|
|
3541
|
-
}
|
|
3542
|
-
if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
|
|
3543
|
-
continue;
|
|
3544
|
-
}
|
|
3545
|
-
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
3546
|
-
let match = linkPattern.exec(line);
|
|
3547
|
-
while (match) {
|
|
3548
|
-
const linkTarget = match[2];
|
|
3549
|
-
if (linkTarget.startsWith("#")) {
|
|
3550
|
-
violations.push({
|
|
3551
|
-
file: filePath,
|
|
3552
|
-
message: `Link in "${currentSection}" must point to a goal file, not an anchor: "${linkTarget}"`,
|
|
3553
|
-
line: i + 1
|
|
3554
|
-
});
|
|
3555
|
-
match = linkPattern.exec(line);
|
|
3556
|
-
continue;
|
|
3557
|
-
}
|
|
3558
|
-
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
3559
|
-
violations.push({
|
|
3560
|
-
file: filePath,
|
|
3561
|
-
message: `Link in "${currentSection}" must point to a goal file, not an external URL: "${linkTarget}"`,
|
|
3562
|
-
line: i + 1
|
|
3563
|
-
});
|
|
3564
|
-
match = linkPattern.exec(line);
|
|
3565
|
-
continue;
|
|
3566
|
-
}
|
|
3567
|
-
const targetPath = linkTarget.split("#")[0];
|
|
3568
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
3569
|
-
if (!resolvedPath.includes("/.dust/goals/")) {
|
|
3570
|
-
violations.push({
|
|
3571
|
-
file: filePath,
|
|
3572
|
-
message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
|
|
3573
|
-
line: i + 1
|
|
3574
|
-
});
|
|
3575
|
-
}
|
|
3576
|
-
match = linkPattern.exec(line);
|
|
3577
|
-
}
|
|
3578
|
-
}
|
|
3579
|
-
return violations;
|
|
3580
|
-
}
|
|
3581
3605
|
function extractGoalRelationships(filePath, content) {
|
|
3582
3606
|
const lines = content.split(`
|
|
3583
3607
|
`);
|
|
@@ -3673,86 +3697,266 @@ function validateNoCycles(allGoalRelationships) {
|
|
|
3673
3697
|
}
|
|
3674
3698
|
return violations;
|
|
3675
3699
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3700
|
+
|
|
3701
|
+
// lib/lint/validators/idea-validator.ts
|
|
3702
|
+
function validateIdeaOpenQuestions(filePath, content) {
|
|
3703
|
+
const violations = [];
|
|
3704
|
+
const lines = content.split(`
|
|
3705
|
+
`);
|
|
3706
|
+
let inOpenQuestions = false;
|
|
3707
|
+
let currentQuestionLine = null;
|
|
3708
|
+
let inCodeBlock = false;
|
|
3709
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3710
|
+
const line = lines[i];
|
|
3711
|
+
if (line.startsWith("```")) {
|
|
3712
|
+
inCodeBlock = !inCodeBlock;
|
|
3713
|
+
continue;
|
|
3681
3714
|
}
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
if (
|
|
3685
|
-
|
|
3715
|
+
if (inCodeBlock)
|
|
3716
|
+
continue;
|
|
3717
|
+
if (line.startsWith("## ")) {
|
|
3718
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
3719
|
+
violations.push({
|
|
3720
|
+
file: filePath,
|
|
3721
|
+
message: "Question has no options listed beneath it",
|
|
3722
|
+
line: currentQuestionLine
|
|
3723
|
+
});
|
|
3724
|
+
}
|
|
3725
|
+
const headingText = line.slice(3).trimEnd();
|
|
3726
|
+
if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
|
|
3727
|
+
violations.push({
|
|
3728
|
+
file: filePath,
|
|
3729
|
+
message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
|
|
3730
|
+
line: i + 1
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3733
|
+
inOpenQuestions = line === "## Open Questions";
|
|
3734
|
+
currentQuestionLine = null;
|
|
3735
|
+
continue;
|
|
3686
3736
|
}
|
|
3687
|
-
|
|
3737
|
+
if (!inOpenQuestions)
|
|
3738
|
+
continue;
|
|
3739
|
+
if (/^[-*] /.test(line.trimStart())) {
|
|
3740
|
+
violations.push({
|
|
3741
|
+
file: filePath,
|
|
3742
|
+
message: "Open Questions must use ### headings for questions and #### headings for options, not bullet points. Run `dust new idea` to see the expected format.",
|
|
3743
|
+
line: i + 1
|
|
3744
|
+
});
|
|
3745
|
+
continue;
|
|
3746
|
+
}
|
|
3747
|
+
if (line.startsWith("### ")) {
|
|
3748
|
+
if (currentQuestionLine !== null) {
|
|
3749
|
+
violations.push({
|
|
3750
|
+
file: filePath,
|
|
3751
|
+
message: "Question has no options listed beneath it",
|
|
3752
|
+
line: currentQuestionLine
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
if (!line.trimEnd().endsWith("?")) {
|
|
3756
|
+
violations.push({
|
|
3757
|
+
file: filePath,
|
|
3758
|
+
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
3759
|
+
line: i + 1
|
|
3760
|
+
});
|
|
3761
|
+
currentQuestionLine = null;
|
|
3762
|
+
} else {
|
|
3763
|
+
currentQuestionLine = i + 1;
|
|
3764
|
+
}
|
|
3765
|
+
continue;
|
|
3766
|
+
}
|
|
3767
|
+
if (line.startsWith("#### ")) {
|
|
3768
|
+
currentQuestionLine = null;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
3772
|
+
violations.push({
|
|
3773
|
+
file: filePath,
|
|
3774
|
+
message: "Question has no options listed beneath it",
|
|
3775
|
+
line: currentQuestionLine
|
|
3776
|
+
});
|
|
3688
3777
|
}
|
|
3778
|
+
return violations;
|
|
3689
3779
|
}
|
|
3690
|
-
|
|
3780
|
+
function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
3781
|
+
const title = extractTitle(content);
|
|
3782
|
+
if (!title) {
|
|
3783
|
+
return null;
|
|
3784
|
+
}
|
|
3785
|
+
for (const prefix of IDEA_TRANSITION_PREFIXES) {
|
|
3786
|
+
if (title.startsWith(prefix)) {
|
|
3787
|
+
const ideaTitle = title.slice(prefix.length);
|
|
3788
|
+
const ideaFilename = titleToFilename(ideaTitle);
|
|
3789
|
+
if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
|
|
3790
|
+
return {
|
|
3791
|
+
file: filePath,
|
|
3792
|
+
message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
|
|
3793
|
+
};
|
|
3794
|
+
}
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
return null;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
// lib/lint/validators/link-validator.ts
|
|
3802
|
+
import { dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
3803
|
+
var SEMANTIC_RULES = [
|
|
3804
|
+
{
|
|
3805
|
+
section: "## Goals",
|
|
3806
|
+
requiredPath: "/.dust/goals/",
|
|
3807
|
+
description: "goal"
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
section: "## Blocked By",
|
|
3811
|
+
requiredPath: "/.dust/tasks/",
|
|
3812
|
+
description: "task"
|
|
3813
|
+
}
|
|
3814
|
+
];
|
|
3815
|
+
function validateLinks(filePath, content, fileSystem) {
|
|
3691
3816
|
const violations = [];
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3817
|
+
const lines = content.split(`
|
|
3818
|
+
`);
|
|
3819
|
+
const fileDir = dirname4(filePath);
|
|
3820
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3821
|
+
const line = lines[i];
|
|
3822
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
3823
|
+
let match = linkPattern.exec(line);
|
|
3824
|
+
while (match) {
|
|
3825
|
+
const linkTarget = match[2];
|
|
3826
|
+
if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
|
|
3827
|
+
const targetPath = linkTarget.split("#")[0];
|
|
3828
|
+
const resolvedPath = resolve2(fileDir, targetPath);
|
|
3829
|
+
if (!fileSystem.exists(resolvedPath)) {
|
|
3830
|
+
violations.push({
|
|
3831
|
+
file: filePath,
|
|
3832
|
+
message: `Broken link: "${linkTarget}"`,
|
|
3833
|
+
line: i + 1
|
|
3834
|
+
});
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
match = linkPattern.exec(line);
|
|
3698
3838
|
}
|
|
3699
|
-
throw error;
|
|
3700
3839
|
}
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3840
|
+
return violations;
|
|
3841
|
+
}
|
|
3842
|
+
function validateSemanticLinks(filePath, content) {
|
|
3843
|
+
const violations = [];
|
|
3844
|
+
const lines = content.split(`
|
|
3845
|
+
`);
|
|
3846
|
+
const fileDir = dirname4(filePath);
|
|
3847
|
+
let currentSection = null;
|
|
3848
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3849
|
+
const line = lines[i];
|
|
3850
|
+
if (line.startsWith("## ")) {
|
|
3851
|
+
currentSection = line;
|
|
3708
3852
|
continue;
|
|
3709
3853
|
}
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
file: entryPath,
|
|
3713
|
-
message: `Subdirectory "${entry}" found in content directory (content directories should be flat)`
|
|
3714
|
-
});
|
|
3854
|
+
const rule = SEMANTIC_RULES.find((r) => r.section === currentSection);
|
|
3855
|
+
if (!rule)
|
|
3715
3856
|
continue;
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3857
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
3858
|
+
let match = linkPattern.exec(line);
|
|
3859
|
+
while (match) {
|
|
3860
|
+
const linkTarget = match[2];
|
|
3861
|
+
if (linkTarget.startsWith("#")) {
|
|
3862
|
+
violations.push({
|
|
3863
|
+
file: filePath,
|
|
3864
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an anchor: "${linkTarget}"`,
|
|
3865
|
+
line: i + 1
|
|
3866
|
+
});
|
|
3867
|
+
match = linkPattern.exec(line);
|
|
3868
|
+
continue;
|
|
3869
|
+
}
|
|
3870
|
+
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
3871
|
+
violations.push({
|
|
3872
|
+
file: filePath,
|
|
3873
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an external URL: "${linkTarget}"`,
|
|
3874
|
+
line: i + 1
|
|
3875
|
+
});
|
|
3876
|
+
match = linkPattern.exec(line);
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
const targetPath = linkTarget.split("#")[0];
|
|
3880
|
+
const resolvedPath = resolve2(fileDir, targetPath);
|
|
3881
|
+
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
3882
|
+
violations.push({
|
|
3883
|
+
file: filePath,
|
|
3884
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file: "${linkTarget}"`,
|
|
3885
|
+
line: i + 1
|
|
3886
|
+
});
|
|
3887
|
+
}
|
|
3888
|
+
match = linkPattern.exec(line);
|
|
3722
3889
|
}
|
|
3723
3890
|
}
|
|
3724
3891
|
return violations;
|
|
3725
3892
|
}
|
|
3726
|
-
|
|
3893
|
+
function validateGoalHierarchyLinks(filePath, content) {
|
|
3727
3894
|
const violations = [];
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3895
|
+
const lines = content.split(`
|
|
3896
|
+
`);
|
|
3897
|
+
const fileDir = dirname4(filePath);
|
|
3898
|
+
let currentSection = null;
|
|
3899
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3900
|
+
const line = lines[i];
|
|
3901
|
+
if (line.startsWith("## ")) {
|
|
3902
|
+
currentSection = line;
|
|
3903
|
+
continue;
|
|
3734
3904
|
}
|
|
3735
|
-
|
|
3736
|
-
}
|
|
3737
|
-
const allowedDirectories = new Set([
|
|
3738
|
-
...EXPECTED_DIRECTORIES,
|
|
3739
|
-
...extraDirectories
|
|
3740
|
-
]);
|
|
3741
|
-
for (const entry of entries) {
|
|
3742
|
-
const entryPath = `${dustPath}/${entry}`;
|
|
3743
|
-
if (!fileSystem.isDirectory(entryPath)) {
|
|
3905
|
+
if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
|
|
3744
3906
|
continue;
|
|
3745
3907
|
}
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3908
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
3909
|
+
let match = linkPattern.exec(line);
|
|
3910
|
+
while (match) {
|
|
3911
|
+
const linkTarget = match[2];
|
|
3912
|
+
if (linkTarget.startsWith("#")) {
|
|
3913
|
+
violations.push({
|
|
3914
|
+
file: filePath,
|
|
3915
|
+
message: `Link in "${currentSection}" must point to a goal file, not an anchor: "${linkTarget}"`,
|
|
3916
|
+
line: i + 1
|
|
3917
|
+
});
|
|
3918
|
+
match = linkPattern.exec(line);
|
|
3919
|
+
continue;
|
|
3920
|
+
}
|
|
3921
|
+
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
3922
|
+
violations.push({
|
|
3923
|
+
file: filePath,
|
|
3924
|
+
message: `Link in "${currentSection}" must point to a goal file, not an external URL: "${linkTarget}"`,
|
|
3925
|
+
line: i + 1
|
|
3926
|
+
});
|
|
3927
|
+
match = linkPattern.exec(line);
|
|
3928
|
+
continue;
|
|
3929
|
+
}
|
|
3930
|
+
const targetPath = linkTarget.split("#")[0];
|
|
3931
|
+
const resolvedPath = resolve2(fileDir, targetPath);
|
|
3932
|
+
if (!resolvedPath.includes("/.dust/goals/")) {
|
|
3933
|
+
violations.push({
|
|
3934
|
+
file: filePath,
|
|
3935
|
+
message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
|
|
3936
|
+
line: i + 1
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
match = linkPattern.exec(line);
|
|
3752
3940
|
}
|
|
3753
3941
|
}
|
|
3754
3942
|
return violations;
|
|
3755
3943
|
}
|
|
3944
|
+
|
|
3945
|
+
// lib/cli/commands/lint-markdown.ts
|
|
3946
|
+
async function safeScanDir(glob, dirPath) {
|
|
3947
|
+
const files = [];
|
|
3948
|
+
try {
|
|
3949
|
+
for await (const file of glob.scan(dirPath)) {
|
|
3950
|
+
files.push(file);
|
|
3951
|
+
}
|
|
3952
|
+
return { files, exists: true };
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
if (error.code === "ENOENT") {
|
|
3955
|
+
return { files: [], exists: false };
|
|
3956
|
+
}
|
|
3957
|
+
throw error;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3756
3960
|
async function lintMarkdown(dependencies) {
|
|
3757
3961
|
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
3758
3962
|
const dustPath = `${context.cwd}/.dust`;
|
|
@@ -3766,7 +3970,7 @@ async function lintMarkdown(dependencies) {
|
|
|
3766
3970
|
const violations = [];
|
|
3767
3971
|
context.stdout("Validating directory structure...");
|
|
3768
3972
|
violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
|
|
3769
|
-
const settingsPath =
|
|
3973
|
+
const settingsPath = join10(dustPath, "config", "settings.json");
|
|
3770
3974
|
if (fileSystem.exists(settingsPath)) {
|
|
3771
3975
|
context.stdout("Validating settings.json...");
|
|
3772
3976
|
try {
|
|
@@ -3927,12 +4131,16 @@ async function lintMarkdown(dependencies) {
|
|
|
3927
4131
|
}
|
|
3928
4132
|
|
|
3929
4133
|
// lib/cli/commands/check.ts
|
|
4134
|
+
var log5 = createLogger("dust.cli.commands.check");
|
|
3930
4135
|
var DEFAULT_CHECK_TIMEOUT_MS = 13000;
|
|
3931
4136
|
async function runSingleCheck(check, cwd, runner) {
|
|
3932
4137
|
const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
4138
|
+
log5(`running check ${check.name}: ${check.command}`);
|
|
3933
4139
|
const startTime = Date.now();
|
|
3934
4140
|
const result = await runner.run(check.command, cwd, timeoutMs);
|
|
3935
4141
|
const durationMs = Date.now() - startTime;
|
|
4142
|
+
const status = result.timedOut ? "timed out" : result.exitCode === 0 ? "passed" : "failed";
|
|
4143
|
+
log5(`check ${check.name} ${status} (${durationMs}ms)`);
|
|
3936
4144
|
return {
|
|
3937
4145
|
name: check.name,
|
|
3938
4146
|
command: check.command,
|
|
@@ -3962,6 +4170,7 @@ async function runValidationCheck(dependencies) {
|
|
|
3962
4170
|
stdout: (msg) => outputLines.push(msg),
|
|
3963
4171
|
stderr: (msg) => outputLines.push(msg)
|
|
3964
4172
|
};
|
|
4173
|
+
log5("running built-in check: dust lint");
|
|
3965
4174
|
const startTime = Date.now();
|
|
3966
4175
|
const result = await lintMarkdown({
|
|
3967
4176
|
...dependencies,
|
|
@@ -3969,6 +4178,8 @@ async function runValidationCheck(dependencies) {
|
|
|
3969
4178
|
arguments: []
|
|
3970
4179
|
});
|
|
3971
4180
|
const durationMs = Date.now() - startTime;
|
|
4181
|
+
const lintStatus = result.exitCode === 0 ? "passed" : "failed";
|
|
4182
|
+
log5(`built-in check dust lint ${lintStatus} (${durationMs}ms)`);
|
|
3972
4183
|
return {
|
|
3973
4184
|
name: "lint",
|
|
3974
4185
|
command: "dust lint",
|
|
@@ -4024,6 +4235,7 @@ async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
|
4024
4235
|
settings
|
|
4025
4236
|
} = dependencies;
|
|
4026
4237
|
const serial = commandArguments.includes("--serial");
|
|
4238
|
+
setLogScope("check");
|
|
4027
4239
|
if (!settings.checks || settings.checks.length === 0) {
|
|
4028
4240
|
context.stderr("Error: No checks configured in .dust/config/settings.json");
|
|
4029
4241
|
context.stderr("");
|
|
@@ -4155,7 +4367,7 @@ function agentsMdContent(dustCommand) {
|
|
|
4155
4367
|
Always immediately run \`${dustCommand} agent\` when you start working in this repository.
|
|
4156
4368
|
`;
|
|
4157
4369
|
}
|
|
4158
|
-
async function
|
|
4370
|
+
async function init2(dependencies) {
|
|
4159
4371
|
const { context, fileSystem } = dependencies;
|
|
4160
4372
|
const colors = getColors();
|
|
4161
4373
|
const dustPath = `${context.cwd}/.dust`;
|
|
@@ -4363,7 +4575,7 @@ var defaultDependencies2 = {
|
|
|
4363
4575
|
createInterface: nodeCreateInterface2
|
|
4364
4576
|
};
|
|
4365
4577
|
async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
|
|
4366
|
-
const { cwd, env } = options;
|
|
4578
|
+
const { cwd, env, signal } = options;
|
|
4367
4579
|
const codexArguments = ["exec", prompt, "--json", "--yolo"];
|
|
4368
4580
|
if (cwd) {
|
|
4369
4581
|
codexArguments.push("--cd", cwd);
|
|
@@ -4375,22 +4587,14 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
|
|
|
4375
4587
|
if (!proc.stdout) {
|
|
4376
4588
|
throw new Error("Failed to get stdout from codex process");
|
|
4377
4589
|
}
|
|
4378
|
-
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
4379
|
-
for await (const line of rl) {
|
|
4380
|
-
if (!line.trim())
|
|
4381
|
-
continue;
|
|
4382
|
-
try {
|
|
4383
|
-
yield JSON.parse(line);
|
|
4384
|
-
} catch {}
|
|
4385
|
-
}
|
|
4386
4590
|
let stderrOutput = "";
|
|
4387
4591
|
proc.stderr?.on("data", (data) => {
|
|
4388
4592
|
stderrOutput += data.toString();
|
|
4389
4593
|
});
|
|
4390
|
-
|
|
4594
|
+
const closePromise = new Promise((resolve3, reject) => {
|
|
4391
4595
|
proc.on("close", (code) => {
|
|
4392
4596
|
if (code === 0 || code === null)
|
|
4393
|
-
|
|
4597
|
+
resolve3();
|
|
4394
4598
|
else {
|
|
4395
4599
|
const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
|
|
4396
4600
|
reject(new Error(errMsg));
|
|
@@ -4398,6 +4602,30 @@ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependenc
|
|
|
4398
4602
|
});
|
|
4399
4603
|
proc.on("error", reject);
|
|
4400
4604
|
});
|
|
4605
|
+
const abortHandler = () => {
|
|
4606
|
+
if (!proc.killed) {
|
|
4607
|
+
proc.kill();
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
if (signal?.aborted) {
|
|
4611
|
+
abortHandler();
|
|
4612
|
+
} else if (signal) {
|
|
4613
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
4614
|
+
}
|
|
4615
|
+
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
4616
|
+
try {
|
|
4617
|
+
for await (const line of rl) {
|
|
4618
|
+
if (!line.trim())
|
|
4619
|
+
continue;
|
|
4620
|
+
try {
|
|
4621
|
+
yield JSON.parse(line);
|
|
4622
|
+
} catch {}
|
|
4623
|
+
}
|
|
4624
|
+
await closePromise;
|
|
4625
|
+
} finally {
|
|
4626
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
4627
|
+
rl.close?.();
|
|
4628
|
+
}
|
|
4401
4629
|
}
|
|
4402
4630
|
|
|
4403
4631
|
// lib/codex/event-parser.ts
|
|
@@ -4785,7 +5013,7 @@ async function facts(dependencies) {
|
|
|
4785
5013
|
|
|
4786
5014
|
// lib/cli/main.ts
|
|
4787
5015
|
var commandRegistry = {
|
|
4788
|
-
init,
|
|
5016
|
+
init: init2,
|
|
4789
5017
|
lint: lintMarkdown,
|
|
4790
5018
|
list,
|
|
4791
5019
|
tasks,
|