@joshski/dust 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/dust.js +251 -193
  2. package/package.json +1 -1
package/dist/dust.js CHANGED
@@ -18,129 +18,128 @@ var KNOWN_CHECK_KEYS = new Set([
18
18
  "hints",
19
19
  "timeoutMilliseconds"
20
20
  ]);
21
- function validateSettingsJson(content) {
21
+ function validateCheckEntry(check, checkPath) {
22
22
  const violations = [];
23
- let parsed;
24
- try {
25
- parsed = JSON.parse(content);
26
- } catch (error) {
27
- violations.push({
28
- message: `Invalid JSON: ${error.message}`
29
- });
23
+ if (typeof check === "string") {
30
24
  return violations;
31
25
  }
32
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
33
- violations.push({
34
- message: "settings.json must be a JSON object"
35
- });
26
+ if (typeof check !== "object" || check === null || Array.isArray(check)) {
27
+ violations.push({ message: `${checkPath} must be a string or object` });
36
28
  return violations;
37
29
  }
38
- const settings = parsed;
39
- for (const key of Object.keys(settings)) {
40
- if (!KNOWN_SETTINGS_KEYS.has(key)) {
30
+ const checkObj = check;
31
+ for (const key of Object.keys(checkObj)) {
32
+ if (!KNOWN_CHECK_KEYS.has(key)) {
41
33
  violations.push({
42
- message: `Unknown key "${key}" in settings.json. Known keys: ${[...KNOWN_SETTINGS_KEYS].sort().join(", ")}`
34
+ message: `Unknown key "${key}" in ${checkPath}. Known keys: ${[...KNOWN_CHECK_KEYS].sort().join(", ")}`
43
35
  });
44
36
  }
45
37
  }
46
- if ("checks" in settings) {
47
- if (!Array.isArray(settings.checks)) {
38
+ if (!("name" in checkObj)) {
39
+ violations.push({
40
+ message: `${checkPath} is missing required field "name"`
41
+ });
42
+ } else if (typeof checkObj.name !== "string") {
43
+ violations.push({ message: `${checkPath}.name must be a string` });
44
+ }
45
+ if (!("command" in checkObj)) {
46
+ violations.push({
47
+ message: `${checkPath} is missing required field "command"`
48
+ });
49
+ } else if (typeof checkObj.command !== "string") {
50
+ violations.push({ message: `${checkPath}.command must be a string` });
51
+ }
52
+ if ("hints" in checkObj) {
53
+ if (!Array.isArray(checkObj.hints)) {
48
54
  violations.push({
49
- message: '"checks" must be an array'
55
+ message: `${checkPath}.hints must be an array of strings`
50
56
  });
51
57
  } else {
52
- for (let i = 0;i < settings.checks.length; i++) {
53
- const check = settings.checks[i];
54
- const checkPath = `checks[${i}]`;
55
- if (typeof check === "string") {
56
- continue;
57
- }
58
- if (typeof check !== "object" || check === null || Array.isArray(check)) {
59
- violations.push({
60
- message: `${checkPath} must be a string or object`
61
- });
62
- continue;
63
- }
64
- const checkObj = check;
65
- for (const key of Object.keys(checkObj)) {
66
- if (!KNOWN_CHECK_KEYS.has(key)) {
67
- violations.push({
68
- message: `Unknown key "${key}" in ${checkPath}. Known keys: ${[...KNOWN_CHECK_KEYS].sort().join(", ")}`
69
- });
70
- }
71
- }
72
- if (!("name" in checkObj)) {
73
- violations.push({
74
- message: `${checkPath} is missing required field "name"`
75
- });
76
- } else if (typeof checkObj.name !== "string") {
58
+ for (let j = 0;j < checkObj.hints.length; j++) {
59
+ if (typeof checkObj.hints[j] !== "string") {
77
60
  violations.push({
78
- message: `${checkPath}.name must be a string`
61
+ message: `${checkPath}.hints[${j}] must be a string`
79
62
  });
80
63
  }
81
- if (!("command" in checkObj)) {
82
- violations.push({
83
- message: `${checkPath} is missing required field "command"`
84
- });
85
- } else if (typeof checkObj.command !== "string") {
86
- violations.push({
87
- message: `${checkPath}.command must be a string`
88
- });
89
- }
90
- if ("hints" in checkObj) {
91
- if (!Array.isArray(checkObj.hints)) {
92
- violations.push({
93
- message: `${checkPath}.hints must be an array of strings`
94
- });
95
- } else {
96
- for (let j = 0;j < checkObj.hints.length; j++) {
97
- if (typeof checkObj.hints[j] !== "string") {
98
- violations.push({
99
- message: `${checkPath}.hints[${j}] must be a string`
100
- });
101
- }
102
- }
103
- }
104
- }
105
- if ("timeoutMilliseconds" in checkObj) {
106
- if (typeof checkObj.timeoutMilliseconds !== "number" || checkObj.timeoutMilliseconds <= 0) {
107
- violations.push({
108
- message: `${checkPath}.timeoutMilliseconds must be a positive number`
109
- });
110
- }
111
- }
112
64
  }
113
65
  }
114
66
  }
115
- if ("extraDirectories" in settings) {
116
- if (!Array.isArray(settings.extraDirectories)) {
67
+ if ("timeoutMilliseconds" in checkObj) {
68
+ if (typeof checkObj.timeoutMilliseconds !== "number" || checkObj.timeoutMilliseconds <= 0) {
117
69
  violations.push({
118
- message: '"extraDirectories" must be an array of strings'
70
+ message: `${checkPath}.timeoutMilliseconds must be a positive number`
119
71
  });
120
- } else {
121
- for (let i = 0;i < settings.extraDirectories.length; i++) {
122
- if (typeof settings.extraDirectories[i] !== "string") {
123
- violations.push({
124
- message: `extraDirectories[${i}] must be a string`
125
- });
126
- }
127
- }
128
72
  }
129
73
  }
130
- if ("dustCommand" in settings && typeof settings.dustCommand !== "string") {
131
- violations.push({
132
- message: '"dustCommand" must be a string'
133
- });
74
+ return violations;
75
+ }
76
+ function validateChecksConfig(settings) {
77
+ if (!("checks" in settings)) {
78
+ return [];
134
79
  }
135
- if ("installCommand" in settings && typeof settings.installCommand !== "string") {
80
+ if (!Array.isArray(settings.checks)) {
81
+ return [{ message: '"checks" must be an array' }];
82
+ }
83
+ const violations = [];
84
+ for (let i = 0;i < settings.checks.length; i++) {
85
+ violations.push(...validateCheckEntry(settings.checks[i], `checks[${i}]`));
86
+ }
87
+ return violations;
88
+ }
89
+ function validateExtraDirectories(settings) {
90
+ if (!("extraDirectories" in settings)) {
91
+ return [];
92
+ }
93
+ if (!Array.isArray(settings.extraDirectories)) {
94
+ return [{ message: '"extraDirectories" must be an array of strings' }];
95
+ }
96
+ const violations = [];
97
+ for (let i = 0;i < settings.extraDirectories.length; i++) {
98
+ if (typeof settings.extraDirectories[i] !== "string") {
99
+ violations.push({ message: `extraDirectories[${i}] must be a string` });
100
+ }
101
+ }
102
+ return violations;
103
+ }
104
+ function validateDustEventsUrl(settings) {
105
+ if ("eventsUrl" in settings && typeof settings.eventsUrl !== "string") {
106
+ return [{ message: '"eventsUrl" must be a string' }];
107
+ }
108
+ return [];
109
+ }
110
+ function validateSettingsJson(content) {
111
+ const violations = [];
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(content);
115
+ } catch (error) {
136
116
  violations.push({
137
- message: '"installCommand" must be a string'
117
+ message: `Invalid JSON: ${error.message}`
138
118
  });
119
+ return violations;
139
120
  }
140
- if ("eventsUrl" in settings && typeof settings.eventsUrl !== "string") {
121
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
141
122
  violations.push({
142
- message: '"eventsUrl" must be a string'
123
+ message: "settings.json must be a JSON object"
143
124
  });
125
+ return violations;
126
+ }
127
+ const settings = parsed;
128
+ for (const key of Object.keys(settings)) {
129
+ if (!KNOWN_SETTINGS_KEYS.has(key)) {
130
+ violations.push({
131
+ message: `Unknown key "${key}" in settings.json. Known keys: ${[...KNOWN_SETTINGS_KEYS].sort().join(", ")}`
132
+ });
133
+ }
134
+ }
135
+ violations.push(...validateChecksConfig(settings));
136
+ violations.push(...validateExtraDirectories(settings));
137
+ violations.push(...validateDustEventsUrl(settings));
138
+ if ("dustCommand" in settings && typeof settings.dustCommand !== "string") {
139
+ violations.push({ message: '"dustCommand" must be a string' });
140
+ }
141
+ if ("installCommand" in settings && typeof settings.installCommand !== "string") {
142
+ violations.push({ message: '"installCommand" must be a string' });
144
143
  }
145
144
  return violations;
146
145
  }
@@ -972,7 +971,8 @@ import { spawn as nodeSpawn3 } from "node:child_process";
972
971
  import { accessSync, statSync } from "node:fs";
973
972
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
974
973
  import { createServer as httpCreateServer } from "node:http";
975
- import { homedir, tmpdir } from "node:os";
974
+ import { homedir } from "node:os";
975
+ import { join as join8 } from "node:path";
976
976
 
977
977
  // lib/bucket/auth.ts
978
978
  import { join as join4 } from "node:path";
@@ -1045,7 +1045,29 @@ async function authenticate(authDeps) {
1045
1045
  if (code) {
1046
1046
  cleanup();
1047
1047
  exchange(code).then(resolve, reject);
1048
- return new Response("<html><body><p>Authentication successful! You can close this tab.</p></body></html>", { headers: { "Content-Type": "text/html" } });
1048
+ return new Response(`<!DOCTYPE html>
1049
+ <html>
1050
+ <head>
1051
+ <meta charset="utf-8">
1052
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1053
+ <title>Authorized</title>
1054
+ <style>
1055
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
1056
+ .card { text-align: center; padding: 2rem; border: 1px solid #333; border-radius: 8px; max-width: 400px; }
1057
+ h1 { font-size: 1.25rem; margin-bottom: 1rem; }
1058
+ p { color: #999; }
1059
+ </style>
1060
+ </head>
1061
+ <body>
1062
+ <div class="card">
1063
+ <svg width="64" height="64" viewBox="0 0 512 512" fill="#fafafa" xmlns="http://www.w3.org/2000/svg" style="margin-bottom: 1rem;">
1064
+ <path d="M392.566 159.4c-.649-79.601-31.851-134.481-80.25-141.944l-1.443-10.127a8.52 8.52 0 0 0-3.339-5.619 8.52 8.52 0 0 0-6.327-1.622l-92.99 13.287c-4.671.666-7.916 4.995-7.25 9.666l1.605 11.229c-.709.179-1.417.307-2.075.692-52.122 30.126-68.688 71.677-77.346 122.859-34.293 13.773-55.008 33.157-55.008 55.316 0 11.997 6.242 23.149 17.24 33.072-.12.922 42.712 207.763 42.712 207.763.06.273.128.555.213.828 10.64 33.678 63.146 57.203 127.693 57.203 66.963 0 118.308-23.107 127.906-58.031 0 0 42.832-206.841 42.712-207.763 10.998-9.922 17.24-21.074 17.24-33.072.001-21.34-19.264-40.083-51.293-53.737m-276.281 51.072a2180 2180 0 0 1-4.022 33.712c-17.326-9.606-27.043-20.545-27.043-31.048 0-11.989 12.723-24.541 34.933-35.044-1.374 10.452-2.612 21.219-3.868 32.38m88.849-158.254.658 4.637a8.52 8.52 0 0 0 3.339 5.619 8.5 8.5 0 0 0 5.123 1.708c.401 0 .811-.026 1.213-.085l92.99-13.287c4.671-.666 7.916-4.995 7.25-9.666l-.837-5.858c35.497 9.077 58.509 53.642 60.482 117.634-32.021-10.477-73.214-16.634-119.35-16.634-43.797 0-83.051 5.593-114.312 15.123 8.205-41.8 23.439-74.573 63.444-99.191m50.867 220.691c-52.891 0-97.217-8.735-127.505-21.1 1.819-13.645 3.339-26.778 4.756-39.425 1.648-14.704 3.228-28.555 5.149-41.653 29.724-10.341 70.208-17.368 117.6-17.368 100.641 0 170.781 31.5 170.781 59.773s-70.14 59.773-170.781 59.773"/>
1065
+ </svg>
1066
+ <h1>Your agent is connected!</h1>
1067
+ <p>You can close this tab.</p>
1068
+ </div>
1069
+ </body>
1070
+ </html>`, { headers: { "Content-Type": "text/html" } });
1049
1071
  }
1050
1072
  return new Response("Missing code", { status: 400 });
1051
1073
  }
@@ -1119,26 +1141,7 @@ function getLogLines(buffer) {
1119
1141
  }
1120
1142
 
1121
1143
  // lib/bucket/repository.ts
1122
- import { join as join6 } from "node:path";
1123
-
1124
- // lib/agent-events.ts
1125
- function rawEventToAgentEvent(rawEvent) {
1126
- if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
1127
- return { type: "agent-session-activity" };
1128
- }
1129
- return { type: "claude-event", rawEvent };
1130
- }
1131
- function formatAgentEvent(event) {
1132
- switch (event.type) {
1133
- case "agent-session-started":
1134
- return `\uD83E\uDD16 Starting Claude: ${event.title}`;
1135
- case "agent-session-ended":
1136
- return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1137
- case "agent-session-activity":
1138
- case "claude-event":
1139
- return null;
1140
- }
1141
- }
1144
+ import { dirname as dirname2, join as join7 } from "node:path";
1142
1145
 
1143
1146
  // lib/claude/spawn-claude-code.ts
1144
1147
  import { spawn as nodeSpawn } from "node:child_process";
@@ -1539,11 +1542,74 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1539
1542
  await dependencies.streamEvents(events, sink, onRawEvent);
1540
1543
  }
1541
1544
 
1545
+ // lib/bucket/repository-git.ts
1546
+ import { join as join5 } from "node:path";
1547
+ function getRepoPath(repoName, reposDir) {
1548
+ const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
1549
+ return join5(reposDir, safeName);
1550
+ }
1551
+ async function cloneRepository(repository, targetPath, spawn, context) {
1552
+ return new Promise((resolve) => {
1553
+ const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
1554
+ stdio: ["ignore", "pipe", "pipe"]
1555
+ });
1556
+ let stderr = "";
1557
+ proc.stderr?.on("data", (data) => {
1558
+ stderr += data.toString();
1559
+ });
1560
+ proc.on("close", (code) => {
1561
+ if (code === 0) {
1562
+ resolve(true);
1563
+ } else {
1564
+ context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
1565
+ resolve(false);
1566
+ }
1567
+ });
1568
+ proc.on("error", (error) => {
1569
+ context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
1570
+ resolve(false);
1571
+ });
1572
+ });
1573
+ }
1574
+ async function removeRepository(path, spawn, context) {
1575
+ return new Promise((resolve) => {
1576
+ const proc = spawn("rm", ["-rf", path], {
1577
+ stdio: ["ignore", "pipe", "pipe"]
1578
+ });
1579
+ proc.on("close", (code) => {
1580
+ resolve(code === 0);
1581
+ });
1582
+ proc.on("error", (error) => {
1583
+ context.stderr(`Failed to remove ${path}: ${error.message}`);
1584
+ resolve(false);
1585
+ });
1586
+ });
1587
+ }
1588
+
1589
+ // lib/agent-events.ts
1590
+ function rawEventToAgentEvent(rawEvent) {
1591
+ if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
1592
+ return { type: "agent-session-activity" };
1593
+ }
1594
+ return { type: "claude-event", rawEvent };
1595
+ }
1596
+ function formatAgentEvent(event) {
1597
+ switch (event.type) {
1598
+ case "agent-session-started":
1599
+ return `\uD83E\uDD16 Starting Claude: ${event.title}`;
1600
+ case "agent-session-ended":
1601
+ return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1602
+ case "agent-session-activity":
1603
+ case "claude-event":
1604
+ return null;
1605
+ }
1606
+ }
1607
+
1542
1608
  // lib/cli/commands/loop.ts
1543
1609
  import { spawn as nodeSpawn2 } from "node:child_process";
1544
1610
  import { readFileSync } from "node:fs";
1545
1611
  import os from "node:os";
1546
- import { dirname, join as join5 } from "node:path";
1612
+ import { dirname, join as join6 } from "node:path";
1547
1613
  import { fileURLToPath } from "node:url";
1548
1614
 
1549
1615
  // lib/workflow-tasks.ts
@@ -1687,8 +1753,8 @@ async function next(dependencies) {
1687
1753
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
1688
1754
  function getDustVersion() {
1689
1755
  const candidates = [
1690
- join5(__dirname2, "../../../package.json"),
1691
- join5(__dirname2, "../package.json")
1756
+ join6(__dirname2, "../../../package.json"),
1757
+ join6(__dirname2, "../package.json")
1692
1758
  ];
1693
1759
  for (const candidate of candidates) {
1694
1760
  try {
@@ -1964,68 +2030,8 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
1964
2030
  return { exitCode: 0 };
1965
2031
  }
1966
2032
 
1967
- // lib/bucket/repository.ts
1968
- var SLEEP_INTERVAL_MS2 = 30000;
1969
- function parseRepository(data) {
1970
- if (typeof data === "string") {
1971
- return { name: data, gitUrl: data };
1972
- }
1973
- if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
1974
- const repositoryData = data;
1975
- if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
1976
- const repo = {
1977
- name: repositoryData.name,
1978
- gitUrl: repositoryData.gitUrl
1979
- };
1980
- if (typeof repositoryData.url === "string") {
1981
- repo.url = repositoryData.url;
1982
- }
1983
- return repo;
1984
- }
1985
- }
1986
- return null;
1987
- }
1988
- function getRepoTempPath(repoName, tempDir) {
1989
- const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
1990
- return join6(tempDir, `dust-bucket-${safeName}`);
1991
- }
1992
- async function cloneRepository(repository, targetPath, spawn, context) {
1993
- return new Promise((resolve) => {
1994
- const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
1995
- stdio: ["ignore", "pipe", "pipe"]
1996
- });
1997
- let stderr = "";
1998
- proc.stderr?.on("data", (data) => {
1999
- stderr += data.toString();
2000
- });
2001
- proc.on("close", (code) => {
2002
- if (code === 0) {
2003
- resolve(true);
2004
- } else {
2005
- context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
2006
- resolve(false);
2007
- }
2008
- });
2009
- proc.on("error", (error) => {
2010
- context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
2011
- resolve(false);
2012
- });
2013
- });
2014
- }
2015
- async function removeRepository(path, spawn, context) {
2016
- return new Promise((resolve) => {
2017
- const proc = spawn("rm", ["-rf", path], {
2018
- stdio: ["ignore", "pipe", "pipe"]
2019
- });
2020
- proc.on("close", (code) => {
2021
- resolve(code === 0);
2022
- });
2023
- proc.on("error", (error) => {
2024
- context.stderr(`Failed to remove ${path}: ${error.message}`);
2025
- resolve(false);
2026
- });
2027
- });
2028
- }
2033
+ // lib/bucket/repository-loop.ts
2034
+ var FALLBACK_TIMEOUT_MS = 300000;
2029
2035
  function createNoOpGlobScanner() {
2030
2036
  return {
2031
2037
  scan: async function* () {}
@@ -2119,16 +2125,48 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2119
2125
  }
2120
2126
  });
2121
2127
  if (result === "no_tasks") {
2122
- await sleep(SLEEP_INTERVAL_MS2);
2128
+ await new Promise((resolve) => {
2129
+ repoState.wakeUp = () => {
2130
+ repoState.wakeUp = undefined;
2131
+ resolve();
2132
+ };
2133
+ sleep(FALLBACK_TIMEOUT_MS).then(() => {
2134
+ if (repoState.wakeUp) {
2135
+ repoState.wakeUp = undefined;
2136
+ resolve();
2137
+ }
2138
+ });
2139
+ });
2123
2140
  }
2124
2141
  }
2125
2142
  appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
2126
2143
  }
2144
+ // lib/bucket/repository.ts
2145
+ function parseRepository(data) {
2146
+ if (typeof data === "string") {
2147
+ return { name: data, gitUrl: data };
2148
+ }
2149
+ if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
2150
+ const repositoryData = data;
2151
+ if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
2152
+ const repo = {
2153
+ name: repositoryData.name,
2154
+ gitUrl: repositoryData.gitUrl
2155
+ };
2156
+ if (typeof repositoryData.url === "string") {
2157
+ repo.url = repositoryData.url;
2158
+ }
2159
+ return repo;
2160
+ }
2161
+ }
2162
+ return null;
2163
+ }
2127
2164
  async function addRepository(repository, manager, repoDeps, context) {
2128
2165
  if (manager.repositories.has(repository.name)) {
2129
2166
  return;
2130
2167
  }
2131
- const repoPath = getRepoTempPath(repository.name, repoDeps.getTempDir());
2168
+ const repoPath = getRepoPath(repository.name, repoDeps.getReposDir());
2169
+ await repoDeps.fileSystem.mkdir(dirname2(repoPath), { recursive: true });
2132
2170
  if (repoDeps.fileSystem.exists(repoPath)) {
2133
2171
  await removeRepository(repoPath, repoDeps.spawn, context);
2134
2172
  }
@@ -2167,6 +2205,7 @@ async function removeRepositoryFromManager(repoName, manager, repoDeps, context)
2167
2205
  return;
2168
2206
  }
2169
2207
  repoState.stopRequested = true;
2208
+ repoState.wakeUp?.();
2170
2209
  if (repoState.loopPromise) {
2171
2210
  await Promise.race([repoState.loopPromise, repoDeps.sleep(5000)]);
2172
2211
  }
@@ -2728,7 +2767,7 @@ function createDefaultBucketDependencies() {
2728
2767
  writeStdout: defaultWriteStdout,
2729
2768
  isTTY: process.stdout.isTTY ?? false,
2730
2769
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2731
- getTempDir: () => tmpdir(),
2770
+ getReposDir: () => process.env.DUST_REPOS_DIR || join8(homedir(), ".dust", "repos"),
2732
2771
  auth: {
2733
2772
  createServer: defaultCreateServer,
2734
2773
  openBrowser: defaultOpenBrowser,
@@ -2766,7 +2805,7 @@ function toRepositoryDependencies(bucketDeps, fileSystem) {
2766
2805
  run,
2767
2806
  fileSystem,
2768
2807
  sleep: bucketDeps.sleep,
2769
- getTempDir: bucketDeps.getTempDir
2808
+ getReposDir: bucketDeps.getReposDir
2770
2809
  };
2771
2810
  }
2772
2811
  function syncUIWithRepoList(state, repos) {
@@ -2874,6 +2913,10 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
2874
2913
  state.emit(disconnectEvent);
2875
2914
  logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
2876
2915
  state.ws = null;
2916
+ if (event.code === 4000) {
2917
+ logMessage(state, context, useTUI, "Another connection replaced this one. Not reconnecting.");
2918
+ return;
2919
+ }
2877
2920
  if (!state.shuttingDown) {
2878
2921
  logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
2879
2922
  state.reconnectTimer = setTimeout(() => {
@@ -2894,9 +2937,23 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
2894
2937
  syncUIWithRepoList(state, repos);
2895
2938
  const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
2896
2939
  const repoContext = createTUIContext(state, context, useTUI);
2897
- handleRepositoryList(repos, state, repoDeps, repoContext).then(() => syncTUI(state)).catch((error) => {
2940
+ handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
2941
+ syncTUI(state);
2942
+ for (const repoData of repos) {
2943
+ if (typeof repoData === "object" && repoData !== null && "name" in repoData && "hasTask" in repoData && repoData.hasTask) {
2944
+ const repoState = state.repositories.get(repoData.name);
2945
+ repoState?.wakeUp?.();
2946
+ }
2947
+ }
2948
+ }).catch((error) => {
2898
2949
  logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
2899
2950
  });
2951
+ } else if (message.type === "task-available") {
2952
+ const repoName = message.repository;
2953
+ if (typeof repoName === "string") {
2954
+ const repoState = state.repositories.get(repoName);
2955
+ repoState?.wakeUp?.();
2956
+ }
2900
2957
  }
2901
2958
  } catch {
2902
2959
  logMessage(state, context, useTUI, `Failed to parse WebSocket message: ${event.data}`, "stderr");
@@ -2918,6 +2975,7 @@ async function shutdown(state, bucketDeps, context) {
2918
2975
  }
2919
2976
  for (const repoState of state.repositories.values()) {
2920
2977
  repoState.stopRequested = true;
2978
+ repoState.wakeUp?.();
2921
2979
  }
2922
2980
  const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
2923
2981
  await Promise.all(loopPromises.map((p) => p.catch(() => {})));
@@ -3097,7 +3155,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3097
3155
  }
3098
3156
 
3099
3157
  // lib/cli/commands/lint-markdown.ts
3100
- import { dirname as dirname2, join as join7, resolve } from "node:path";
3158
+ import { dirname as dirname3, join as join9, resolve } from "node:path";
3101
3159
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
3102
3160
  var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
3103
3161
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
@@ -3199,7 +3257,7 @@ function validateLinks(filePath, content, fileSystem) {
3199
3257
  const violations = [];
3200
3258
  const lines = content.split(`
3201
3259
  `);
3202
- const fileDir = dirname2(filePath);
3260
+ const fileDir = dirname3(filePath);
3203
3261
  for (let i = 0;i < lines.length; i++) {
3204
3262
  const line = lines[i];
3205
3263
  const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
@@ -3316,7 +3374,7 @@ function validateSemanticLinks(filePath, content) {
3316
3374
  const violations = [];
3317
3375
  const lines = content.split(`
3318
3376
  `);
3319
- const fileDir = dirname2(filePath);
3377
+ const fileDir = dirname3(filePath);
3320
3378
  let currentSection = null;
3321
3379
  for (let i = 0;i < lines.length; i++) {
3322
3380
  const line = lines[i];
@@ -3399,7 +3457,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3399
3457
  const violations = [];
3400
3458
  const lines = content.split(`
3401
3459
  `);
3402
- const fileDir = dirname2(filePath);
3460
+ const fileDir = dirname3(filePath);
3403
3461
  let currentSection = null;
3404
3462
  for (let i = 0;i < lines.length; i++) {
3405
3463
  const line = lines[i];
@@ -3449,7 +3507,7 @@ function validateGoalHierarchyLinks(filePath, content) {
3449
3507
  function extractGoalRelationships(filePath, content) {
3450
3508
  const lines = content.split(`
3451
3509
  `);
3452
- const fileDir = dirname2(filePath);
3510
+ const fileDir = dirname3(filePath);
3453
3511
  const parentGoals = [];
3454
3512
  const subGoals = [];
3455
3513
  let currentSection = null;
@@ -3634,7 +3692,7 @@ async function lintMarkdown(dependencies) {
3634
3692
  const violations = [];
3635
3693
  context.stdout("Validating directory structure...");
3636
3694
  violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
3637
- const settingsPath = join7(dustPath, "config", "settings.json");
3695
+ const settingsPath = join9(dustPath, "config", "settings.json");
3638
3696
  if (fileSystem.exists(settingsPath)) {
3639
3697
  context.stdout("Validating settings.json...");
3640
3698
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {