@joshski/dust 0.1.44 → 0.1.46

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.
@@ -39,8 +39,4 @@ export interface EventMessage {
39
39
  * is forwarded as a claude-event.
40
40
  */
41
41
  export declare function rawEventToAgentEvent(rawEvent: Record<string, unknown>): AgentSessionEvent;
42
- /**
43
- * Format an AgentSessionEvent for console output.
44
- * Returns null for events that should not be displayed.
45
- */
46
42
  export declare function formatAgentEvent(event: AgentSessionEvent): string | null;
@@ -22,6 +22,7 @@ export interface FileSystem {
22
22
  readdir: (path: string) => Promise<string[]>;
23
23
  chmod: (path: string, mode: number) => Promise<void>;
24
24
  isDirectory: (path: string) => boolean;
25
+ getFileCreationTime: (path: string) => number;
25
26
  }
26
27
  export interface GlobScanner {
27
28
  scan: (dir: string) => AsyncIterable<string>;
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") {
77
- violations.push({
78
- message: `${checkPath}.name must be a string`
79
- });
80
- }
81
- if (!("command" in checkObj)) {
82
- violations.push({
83
- message: `${checkPath} is missing required field "command"`
84
- });
85
- } else if (typeof checkObj.command !== "string") {
58
+ for (let j = 0;j < checkObj.hints.length; j++) {
59
+ if (typeof checkObj.hints[j] !== "string") {
86
60
  violations.push({
87
- message: `${checkPath}.command must be a string`
61
+ message: `${checkPath}.hints[${j}] must be a string`
88
62
  });
89
63
  }
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
  }
@@ -578,6 +577,10 @@ function agentDeveloperExperience() {
578
577
  4. **Debugging tools** - Can agents diagnose issues without trial and error?
579
578
  5. **Structured logging** - Is system behavior observable through logs?
580
579
 
580
+ ## Goals
581
+
582
+ (none)
583
+
581
584
  ## Blocked By
582
585
 
583
586
  (none)
@@ -607,6 +610,10 @@ function deadCode() {
607
610
  4. **Unused dependencies** - Packages in package.json not used in code
608
611
  5. **Commented-out code** - Old code left in comments
609
612
 
613
+ ## Goals
614
+
615
+ (none)
616
+
610
617
  ## Blocked By
611
618
 
612
619
  (none)
@@ -637,6 +644,10 @@ function factsVerification() {
637
644
  3. **Staleness** - Have facts become outdated due to recent changes?
638
645
  4. **Relevance** - Are all facts still useful for understanding the project?
639
646
 
647
+ ## Goals
648
+
649
+ (none)
650
+
640
651
  ## Blocked By
641
652
 
642
653
  (none)
@@ -666,6 +677,10 @@ function ideasFromCommits() {
666
677
  3. **Pattern opportunities** - Can recent changes be generalized?
667
678
  4. **Test gaps** - Do recent changes have adequate test coverage?
668
679
 
680
+ ## Goals
681
+
682
+ (none)
683
+
669
684
  ## Blocked By
670
685
 
671
686
  (none)
@@ -694,6 +709,10 @@ function ideasFromGoals() {
694
709
  3. **New opportunities** - What work would better achieve each goal?
695
710
  4. **Goal alignment** - Are current tasks aligned with stated goals?
696
711
 
712
+ ## Goals
713
+
714
+ (none)
715
+
697
716
  ## Blocked By
698
717
 
699
718
  (none)
@@ -722,6 +741,10 @@ function performanceReview() {
722
741
  4. **Build performance** - How fast is the build process?
723
742
  5. **Test speed** - Are tests running efficiently?
724
743
 
744
+ ## Goals
745
+
746
+ (none)
747
+
725
748
  ## Blocked By
726
749
 
727
750
  (none)
@@ -751,6 +774,10 @@ function securityReview() {
751
774
  4. **Sensitive data exposure** - Logging sensitive data, insecure storage
752
775
  5. **Dependency vulnerabilities** - Known CVEs in dependencies
753
776
 
777
+ ## Goals
778
+
779
+ (none)
780
+
754
781
  ## Blocked By
755
782
 
756
783
  (none)
@@ -781,6 +808,10 @@ function staleIdeas() {
781
808
  3. **Actionability** - Can the idea be converted to a task?
782
809
  4. **Duplication** - Are there overlapping or redundant ideas?
783
810
 
811
+ ## Goals
812
+
813
+ (none)
814
+
784
815
  ## Blocked By
785
816
 
786
817
  (none)
@@ -810,6 +841,10 @@ function testCoverage() {
810
841
  4. **User-facing features** - UI components, form validation
811
842
  5. **Recent changes** - Code modified in the last few commits
812
843
 
844
+ ## Goals
845
+
846
+ (none)
847
+
813
848
  ## Blocked By
814
849
 
815
850
  (none)
@@ -973,7 +1008,7 @@ import { accessSync, statSync } from "node:fs";
973
1008
  import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
974
1009
  import { createServer as httpCreateServer } from "node:http";
975
1010
  import { homedir } from "node:os";
976
- import { join as join7 } from "node:path";
1011
+ import { join as join8 } from "node:path";
977
1012
 
978
1013
  // lib/bucket/auth.ts
979
1014
  import { join as join4 } from "node:path";
@@ -1046,7 +1081,29 @@ async function authenticate(authDeps) {
1046
1081
  if (code) {
1047
1082
  cleanup();
1048
1083
  exchange(code).then(resolve, reject);
1049
- return new Response("<html><body><p>Authentication successful! You can close this tab.</p></body></html>", { headers: { "Content-Type": "text/html" } });
1084
+ return new Response(`<!DOCTYPE html>
1085
+ <html>
1086
+ <head>
1087
+ <meta charset="utf-8">
1088
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1089
+ <title>Authorized</title>
1090
+ <style>
1091
+ body { font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; background: #0a0a0a; color: #fafafa; }
1092
+ .card { text-align: center; padding: 2rem; border: 1px solid #333; border-radius: 8px; max-width: 400px; }
1093
+ h1 { font-size: 1.25rem; margin-bottom: 1rem; }
1094
+ p { color: #999; }
1095
+ </style>
1096
+ </head>
1097
+ <body>
1098
+ <div class="card">
1099
+ <svg width="64" height="64" viewBox="0 0 512 512" fill="#fafafa" xmlns="http://www.w3.org/2000/svg" style="margin-bottom: 1rem;">
1100
+ <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"/>
1101
+ </svg>
1102
+ <h1>Your agent is connected!</h1>
1103
+ <p>You can close this tab.</p>
1104
+ </div>
1105
+ </body>
1106
+ </html>`, { headers: { "Content-Type": "text/html" } });
1050
1107
  }
1051
1108
  return new Response("Missing code", { status: 400 });
1052
1109
  }
@@ -1120,26 +1177,7 @@ function getLogLines(buffer) {
1120
1177
  }
1121
1178
 
1122
1179
  // lib/bucket/repository.ts
1123
- import { dirname as dirname2, join as join6 } from "node:path";
1124
-
1125
- // lib/agent-events.ts
1126
- function rawEventToAgentEvent(rawEvent) {
1127
- if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
1128
- return { type: "agent-session-activity" };
1129
- }
1130
- return { type: "claude-event", rawEvent };
1131
- }
1132
- function formatAgentEvent(event) {
1133
- switch (event.type) {
1134
- case "agent-session-started":
1135
- return `\uD83E\uDD16 Starting Claude: ${event.title}`;
1136
- case "agent-session-ended":
1137
- return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
1138
- case "agent-session-activity":
1139
- case "claude-event":
1140
- return null;
1141
- }
1142
- }
1180
+ import { dirname as dirname2, join as join7 } from "node:path";
1143
1181
 
1144
1182
  // lib/claude/spawn-claude-code.ts
1145
1183
  import { spawn as nodeSpawn } from "node:child_process";
@@ -1540,11 +1578,81 @@ async function run(prompt, options = {}, dependencies = defaultRunnerDependencie
1540
1578
  await dependencies.streamEvents(events, sink, onRawEvent);
1541
1579
  }
1542
1580
 
1581
+ // lib/bucket/repository-git.ts
1582
+ import { join as join5 } from "node:path";
1583
+ function getRepoPath(repoName, reposDir) {
1584
+ const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
1585
+ return join5(reposDir, safeName);
1586
+ }
1587
+ async function cloneRepository(repository, targetPath, spawn, context) {
1588
+ return new Promise((resolve) => {
1589
+ const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
1590
+ stdio: ["ignore", "pipe", "pipe"]
1591
+ });
1592
+ let stderr = "";
1593
+ proc.stderr?.on("data", (data) => {
1594
+ stderr += data.toString();
1595
+ });
1596
+ proc.on("close", (code) => {
1597
+ if (code === 0) {
1598
+ resolve(true);
1599
+ } else {
1600
+ context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
1601
+ resolve(false);
1602
+ }
1603
+ });
1604
+ proc.on("error", (error) => {
1605
+ context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
1606
+ resolve(false);
1607
+ });
1608
+ });
1609
+ }
1610
+ async function removeRepository(path, spawn, context) {
1611
+ return new Promise((resolve) => {
1612
+ const proc = spawn("rm", ["-rf", path], {
1613
+ stdio: ["ignore", "pipe", "pipe"]
1614
+ });
1615
+ proc.on("close", (code) => {
1616
+ resolve(code === 0);
1617
+ });
1618
+ proc.on("error", (error) => {
1619
+ context.stderr(`Failed to remove ${path}: ${error.message}`);
1620
+ resolve(false);
1621
+ });
1622
+ });
1623
+ }
1624
+
1625
+ // lib/agent-events.ts
1626
+ function rawEventToAgentEvent(rawEvent) {
1627
+ if (typeof rawEvent.type === "string" && rawEvent.type === "stream_event") {
1628
+ return { type: "agent-session-activity" };
1629
+ }
1630
+ return { type: "claude-event", rawEvent };
1631
+ }
1632
+ function agentDisplayName(agentType) {
1633
+ if (agentType === "codex")
1634
+ return "Codex";
1635
+ return "Claude";
1636
+ }
1637
+ function formatAgentEvent(event) {
1638
+ switch (event.type) {
1639
+ case "agent-session-started": {
1640
+ const name = agentDisplayName(event.agentType);
1641
+ return `\uD83E\uDD16 Starting ${name}: ${event.title}`;
1642
+ }
1643
+ case "agent-session-ended":
1644
+ return event.success ? "\uD83E\uDD16 Agent session ended (success)" : `\uD83E\uDD16 Agent session ended (error: ${event.error})`;
1645
+ case "agent-session-activity":
1646
+ case "claude-event":
1647
+ return null;
1648
+ }
1649
+ }
1650
+
1543
1651
  // lib/cli/commands/loop.ts
1544
1652
  import { spawn as nodeSpawn2 } from "node:child_process";
1545
1653
  import { readFileSync } from "node:fs";
1546
1654
  import os from "node:os";
1547
- import { dirname, join as join5 } from "node:path";
1655
+ import { dirname, join as join6 } from "node:path";
1548
1656
  import { fileURLToPath } from "node:url";
1549
1657
 
1550
1658
  // lib/workflow-tasks.ts
@@ -1634,7 +1742,11 @@ async function findUnblockedTasks(cwd, fileSystem) {
1634
1742
  return { tasks: [] };
1635
1743
  }
1636
1744
  const files = await fileSystem.readdir(tasksPath);
1637
- const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
1745
+ const mdFiles = files.filter((f) => f.endsWith(".md")).sort((a, b) => {
1746
+ const aTime = fileSystem.getFileCreationTime(`${tasksPath}/${a}`);
1747
+ const bTime = fileSystem.getFileCreationTime(`${tasksPath}/${b}`);
1748
+ return aTime - bTime;
1749
+ });
1638
1750
  if (mdFiles.length === 0) {
1639
1751
  return { tasks: [] };
1640
1752
  }
@@ -1688,8 +1800,8 @@ async function next(dependencies) {
1688
1800
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
1689
1801
  function getDustVersion() {
1690
1802
  const candidates = [
1691
- join5(__dirname2, "../../../package.json"),
1692
- join5(__dirname2, "../package.json")
1803
+ join6(__dirname2, "../../../package.json"),
1804
+ join6(__dirname2, "../package.json")
1693
1805
  ];
1694
1806
  for (const candidate of candidates) {
1695
1807
  try {
@@ -1712,8 +1824,10 @@ function formatLoopEvent(event) {
1712
1824
  switch (event.type) {
1713
1825
  case "loop.warning":
1714
1826
  return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
1715
- case "loop.started":
1716
- return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
1827
+ case "loop.started": {
1828
+ const agent2 = event.agentType ?? "claude";
1829
+ return `\uD83D\uDD04 Starting dust loop ${agent2} (max ${event.maxIterations} iterations)...`;
1830
+ }
1717
1831
  case "loop.syncing":
1718
1832
  return "\uD83C\uDF0D Syncing with remote";
1719
1833
  case "loop.sync_skipped":
@@ -1799,6 +1913,7 @@ async function findAvailableTasks(dependencies) {
1799
1913
  async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAgentEvent, options = {}) {
1800
1914
  const { context } = dependencies;
1801
1915
  const { spawn, run: run2 } = loopDependencies;
1916
+ const agentName = loopDependencies.agentType === "codex" ? "Codex" : "Claude";
1802
1917
  const { onRawEvent } = options;
1803
1918
  onLoopEvent({ type: "loop.syncing" });
1804
1919
  const pullResult = await gitPull(context.cwd, spawn);
@@ -1823,7 +1938,7 @@ Make sure the repository is in a clean state and synced with remote before finis
1823
1938
  type: "agent-session-started",
1824
1939
  title: "Resolving git conflict",
1825
1940
  prompt: prompt2,
1826
- agentType: "claude",
1941
+ agentType: loopDependencies.agentType ?? "claude",
1827
1942
  purpose: "git-conflict",
1828
1943
  ...getEnvironmentContext(context.cwd)
1829
1944
  });
@@ -1840,7 +1955,7 @@ Make sure the repository is in a clean state and synced with remote before finis
1840
1955
  return "resolved_pull_conflict";
1841
1956
  } catch (error) {
1842
1957
  const errorMessage = error instanceof Error ? error.message : String(error);
1843
- context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
1958
+ context.stderr(`${agentName} failed to resolve git pull conflict: ${errorMessage}`);
1844
1959
  onAgentEvent?.({
1845
1960
  type: "agent-session-ended",
1846
1961
  success: false,
@@ -1876,7 +1991,7 @@ ${instructions}`;
1876
1991
  type: "agent-session-started",
1877
1992
  title: task.title ?? task.path,
1878
1993
  prompt,
1879
- agentType: "claude",
1994
+ agentType: loopDependencies.agentType ?? "claude",
1880
1995
  purpose: "task",
1881
1996
  ...getEnvironmentContext(context.cwd)
1882
1997
  });
@@ -1893,7 +2008,7 @@ ${instructions}`;
1893
2008
  return "ran_claude";
1894
2009
  } catch (error) {
1895
2010
  const errorMessage = error instanceof Error ? error.message : String(error);
1896
- context.stderr(`Claude exited with error: ${errorMessage}`);
2011
+ context.stderr(`${agentName} exited with error: ${errorMessage}`);
1897
2012
  onAgentEvent?.({
1898
2013
  type: "agent-session-ended",
1899
2014
  success: false,
@@ -1937,7 +2052,11 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
1937
2052
  sendWireEvent(event);
1938
2053
  };
1939
2054
  onLoopEvent({ type: "loop.warning" });
1940
- onLoopEvent({ type: "loop.started", maxIterations });
2055
+ onLoopEvent({
2056
+ type: "loop.started",
2057
+ maxIterations,
2058
+ agentType: loopDependencies.agentType
2059
+ });
1941
2060
  context.stdout(" Press Ctrl+C to stop");
1942
2061
  context.stdout("");
1943
2062
  let completedIterations = 0;
@@ -1965,68 +2084,8 @@ async function loopClaude(dependencies, loopDependencies = createDefaultDependen
1965
2084
  return { exitCode: 0 };
1966
2085
  }
1967
2086
 
1968
- // lib/bucket/repository.ts
1969
- var SLEEP_INTERVAL_MS2 = 30000;
1970
- function parseRepository(data) {
1971
- if (typeof data === "string") {
1972
- return { name: data, gitUrl: data };
1973
- }
1974
- if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
1975
- const repositoryData = data;
1976
- if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
1977
- const repo = {
1978
- name: repositoryData.name,
1979
- gitUrl: repositoryData.gitUrl
1980
- };
1981
- if (typeof repositoryData.url === "string") {
1982
- repo.url = repositoryData.url;
1983
- }
1984
- return repo;
1985
- }
1986
- }
1987
- return null;
1988
- }
1989
- function getRepoPath(repoName, reposDir) {
1990
- const safeName = repoName.replace(/[^a-zA-Z0-9-_/]/g, "-");
1991
- return join6(reposDir, safeName);
1992
- }
1993
- async function cloneRepository(repository, targetPath, spawn, context) {
1994
- return new Promise((resolve) => {
1995
- const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
1996
- stdio: ["ignore", "pipe", "pipe"]
1997
- });
1998
- let stderr = "";
1999
- proc.stderr?.on("data", (data) => {
2000
- stderr += data.toString();
2001
- });
2002
- proc.on("close", (code) => {
2003
- if (code === 0) {
2004
- resolve(true);
2005
- } else {
2006
- context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
2007
- resolve(false);
2008
- }
2009
- });
2010
- proc.on("error", (error) => {
2011
- context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
2012
- resolve(false);
2013
- });
2014
- });
2015
- }
2016
- async function removeRepository(path, spawn, context) {
2017
- return new Promise((resolve) => {
2018
- const proc = spawn("rm", ["-rf", path], {
2019
- stdio: ["ignore", "pipe", "pipe"]
2020
- });
2021
- proc.on("close", (code) => {
2022
- resolve(code === 0);
2023
- });
2024
- proc.on("error", (error) => {
2025
- context.stderr(`Failed to remove ${path}: ${error.message}`);
2026
- resolve(false);
2027
- });
2028
- });
2029
- }
2087
+ // lib/bucket/repository-loop.ts
2088
+ var FALLBACK_TIMEOUT_MS = 300000;
2030
2089
  function createNoOpGlobScanner() {
2031
2090
  return {
2032
2091
  scan: async function* () {}
@@ -2120,11 +2179,42 @@ async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
2120
2179
  }
2121
2180
  });
2122
2181
  if (result === "no_tasks") {
2123
- await sleep(SLEEP_INTERVAL_MS2);
2182
+ await new Promise((resolve) => {
2183
+ repoState.wakeUp = () => {
2184
+ repoState.wakeUp = undefined;
2185
+ resolve();
2186
+ };
2187
+ sleep(FALLBACK_TIMEOUT_MS).then(() => {
2188
+ if (repoState.wakeUp) {
2189
+ repoState.wakeUp = undefined;
2190
+ resolve();
2191
+ }
2192
+ });
2193
+ });
2124
2194
  }
2125
2195
  }
2126
2196
  appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
2127
2197
  }
2198
+ // lib/bucket/repository.ts
2199
+ function parseRepository(data) {
2200
+ if (typeof data === "string") {
2201
+ return { name: data, gitUrl: data };
2202
+ }
2203
+ if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
2204
+ const repositoryData = data;
2205
+ if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
2206
+ const repo = {
2207
+ name: repositoryData.name,
2208
+ gitUrl: repositoryData.gitUrl
2209
+ };
2210
+ if (typeof repositoryData.url === "string") {
2211
+ repo.url = repositoryData.url;
2212
+ }
2213
+ return repo;
2214
+ }
2215
+ }
2216
+ return null;
2217
+ }
2128
2218
  async function addRepository(repository, manager, repoDeps, context) {
2129
2219
  if (manager.repositories.has(repository.name)) {
2130
2220
  return;
@@ -2169,6 +2259,7 @@ async function removeRepositoryFromManager(repoName, manager, repoDeps, context)
2169
2259
  return;
2170
2260
  }
2171
2261
  repoState.stopRequested = true;
2262
+ repoState.wakeUp?.();
2172
2263
  if (repoState.loopPromise) {
2173
2264
  await Promise.race([repoState.loopPromise, repoDeps.sleep(5000)]);
2174
2265
  }
@@ -2714,6 +2805,7 @@ function createDefaultBucketDependencies() {
2714
2805
  return false;
2715
2806
  }
2716
2807
  },
2808
+ getFileCreationTime: (path) => statSync(path).birthtimeMs,
2717
2809
  readFile: (path) => readFile(path, "utf8"),
2718
2810
  writeFile: (path, content) => writeFile(path, content, "utf8"),
2719
2811
  mkdir: (path, options) => mkdir(path, options).then(() => {}),
@@ -2730,7 +2822,7 @@ function createDefaultBucketDependencies() {
2730
2822
  writeStdout: defaultWriteStdout,
2731
2823
  isTTY: process.stdout.isTTY ?? false,
2732
2824
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
2733
- getReposDir: () => process.env.DUST_REPOS_DIR || join7(homedir(), ".dust", "repos"),
2825
+ getReposDir: () => process.env.DUST_REPOS_DIR || join8(homedir(), ".dust", "repos"),
2734
2826
  auth: {
2735
2827
  createServer: defaultCreateServer,
2736
2828
  openBrowser: defaultOpenBrowser,
@@ -2876,6 +2968,10 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
2876
2968
  state.emit(disconnectEvent);
2877
2969
  logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
2878
2970
  state.ws = null;
2971
+ if (event.code === 4000) {
2972
+ logMessage(state, context, useTUI, "Another connection replaced this one. Not reconnecting.");
2973
+ return;
2974
+ }
2879
2975
  if (!state.shuttingDown) {
2880
2976
  logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
2881
2977
  state.reconnectTimer = setTimeout(() => {
@@ -2896,9 +2992,23 @@ function connectWebSocket(token, state, bucketDependencies, context, fileSystem,
2896
2992
  syncUIWithRepoList(state, repos);
2897
2993
  const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
2898
2994
  const repoContext = createTUIContext(state, context, useTUI);
2899
- handleRepositoryList(repos, state, repoDeps, repoContext).then(() => syncTUI(state)).catch((error) => {
2995
+ handleRepositoryList(repos, state, repoDeps, repoContext).then(() => {
2996
+ syncTUI(state);
2997
+ for (const repoData of repos) {
2998
+ if (typeof repoData === "object" && repoData !== null && "name" in repoData && "hasTask" in repoData && repoData.hasTask) {
2999
+ const repoState = state.repositories.get(repoData.name);
3000
+ repoState?.wakeUp?.();
3001
+ }
3002
+ }
3003
+ }).catch((error) => {
2900
3004
  logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
2901
3005
  });
3006
+ } else if (message.type === "task-available") {
3007
+ const repoName = message.repository;
3008
+ if (typeof repoName === "string") {
3009
+ const repoState = state.repositories.get(repoName);
3010
+ repoState?.wakeUp?.();
3011
+ }
2902
3012
  }
2903
3013
  } catch {
2904
3014
  logMessage(state, context, useTUI, `Failed to parse WebSocket message: ${event.data}`, "stderr");
@@ -2920,6 +3030,7 @@ async function shutdown(state, bucketDeps, context) {
2920
3030
  }
2921
3031
  for (const repoState of state.repositories.values()) {
2922
3032
  repoState.stopRequested = true;
3033
+ repoState.wakeUp?.();
2923
3034
  }
2924
3035
  const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
2925
3036
  await Promise.all(loopPromises.map((p) => p.catch(() => {})));
@@ -3099,7 +3210,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
3099
3210
  }
3100
3211
 
3101
3212
  // lib/cli/commands/lint-markdown.ts
3102
- import { dirname as dirname3, join as join8, resolve } from "node:path";
3213
+ import { dirname as dirname3, join as join9, resolve } from "node:path";
3103
3214
  var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
3104
3215
  var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
3105
3216
  var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
@@ -3636,7 +3747,7 @@ async function lintMarkdown(dependencies) {
3636
3747
  const violations = [];
3637
3748
  context.stdout("Validating directory structure...");
3638
3749
  violations.push(...await validateDirectoryStructure(dustPath, fileSystem, dependencies.settings.extraDirectories));
3639
- const settingsPath = join8(dustPath, "config", "settings.json");
3750
+ const settingsPath = join9(dustPath, "config", "settings.json");
3640
3751
  if (fileSystem.exists(settingsPath)) {
3641
3752
  context.stdout("Validating settings.json...");
3642
3753
  try {
@@ -4225,6 +4336,125 @@ async function list(dependencies) {
4225
4336
  return { exitCode: 0 };
4226
4337
  }
4227
4338
 
4339
+ // lib/codex/spawn-codex.ts
4340
+ import { spawn as nodeSpawn4 } from "node:child_process";
4341
+ import { createInterface as nodeCreateInterface2 } from "node:readline";
4342
+ var defaultDependencies2 = {
4343
+ spawn: nodeSpawn4,
4344
+ createInterface: nodeCreateInterface2
4345
+ };
4346
+ async function* spawnCodex(prompt, options = {}, dependencies = defaultDependencies2) {
4347
+ const { cwd, env } = options;
4348
+ const codexArguments = ["exec", prompt, "--json", "--yolo"];
4349
+ if (cwd) {
4350
+ codexArguments.push("--cd", cwd);
4351
+ }
4352
+ const proc = dependencies.spawn("codex", codexArguments, {
4353
+ stdio: ["ignore", "pipe", "pipe"],
4354
+ env: { ...process.env, ...env }
4355
+ });
4356
+ if (!proc.stdout) {
4357
+ throw new Error("Failed to get stdout from codex process");
4358
+ }
4359
+ const rl = dependencies.createInterface({ input: proc.stdout });
4360
+ for await (const line of rl) {
4361
+ if (!line.trim())
4362
+ continue;
4363
+ try {
4364
+ yield JSON.parse(line);
4365
+ } catch {}
4366
+ }
4367
+ let stderrOutput = "";
4368
+ proc.stderr?.on("data", (data) => {
4369
+ stderrOutput += data.toString();
4370
+ });
4371
+ await new Promise((resolve2, reject) => {
4372
+ proc.on("close", (code) => {
4373
+ if (code === 0 || code === null)
4374
+ resolve2();
4375
+ else {
4376
+ const errMsg = stderrOutput.trim() ? `codex exited with code ${code}: ${stderrOutput.trim()}` : `codex exited with code ${code}`;
4377
+ reject(new Error(errMsg));
4378
+ }
4379
+ });
4380
+ proc.on("error", reject);
4381
+ });
4382
+ }
4383
+
4384
+ // lib/codex/event-parser.ts
4385
+ function* parseCodexRawEvent(raw) {
4386
+ if (raw.type !== "item.completed")
4387
+ return;
4388
+ const item = raw.item;
4389
+ if (!item)
4390
+ return;
4391
+ if (item.type === "agent_message" && typeof item.text === "string") {
4392
+ yield { type: "text_delta", text: `${item.text}
4393
+ ` };
4394
+ } else if (item.type === "command_execution" && typeof item.command === "string") {
4395
+ yield {
4396
+ type: "tool_use",
4397
+ id: typeof item.id === "string" ? item.id : "",
4398
+ name: "command_execution",
4399
+ input: { command: item.command }
4400
+ };
4401
+ if (typeof item.aggregated_output === "string") {
4402
+ yield {
4403
+ type: "tool_result",
4404
+ toolUseId: typeof item.id === "string" ? item.id : "",
4405
+ content: item.aggregated_output || `(exit code: ${item.exit_code ?? "unknown"})`
4406
+ };
4407
+ }
4408
+ }
4409
+ }
4410
+
4411
+ // lib/codex/streamer.ts
4412
+ async function streamCodexEvents(events, sink, onRawEvent) {
4413
+ let hadTextOutput = false;
4414
+ for await (const raw of events) {
4415
+ onRawEvent?.(raw);
4416
+ for (const event of parseCodexRawEvent(raw)) {
4417
+ processEvent(event, sink, { hadTextOutput });
4418
+ if (event.type === "text_delta") {
4419
+ hadTextOutput = true;
4420
+ } else if (event.type === "tool_use") {
4421
+ hadTextOutput = false;
4422
+ }
4423
+ }
4424
+ }
4425
+ }
4426
+
4427
+ // lib/codex/run.ts
4428
+ var defaultRunnerDependencies2 = {
4429
+ spawnCodex,
4430
+ createStdoutSink,
4431
+ streamCodexEvents
4432
+ };
4433
+ async function run2(prompt, options = {}, dependencies = defaultRunnerDependencies2) {
4434
+ const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
4435
+ const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
4436
+ const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
4437
+ const events = dependencies.spawnCodex(prompt, spawnOptions);
4438
+ const sink = dependencies.createStdoutSink();
4439
+ await dependencies.streamCodexEvents(events, sink, onRawEvent);
4440
+ }
4441
+
4442
+ // lib/cli/commands/loop-codex.ts
4443
+ function createCodexDependencies(overrides = {}) {
4444
+ return {
4445
+ ...createDefaultDependencies(),
4446
+ run: run2,
4447
+ ...overrides,
4448
+ agentType: "codex"
4449
+ };
4450
+ }
4451
+ async function loopCodex(dependencies, loopDependencies = createCodexDependencies()) {
4452
+ return loopClaude(dependencies, {
4453
+ ...loopDependencies,
4454
+ agentType: "codex"
4455
+ });
4456
+ }
4457
+
4228
4458
  // lib/cli/commands/new-goal.ts
4229
4459
  function newGoalInstructions(vars) {
4230
4460
  const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
@@ -4555,6 +4785,7 @@ var commandRegistry = {
4555
4785
  "implement task": implementTask,
4556
4786
  "pick task": pickTask,
4557
4787
  "loop claude": loopClaude,
4788
+ "loop codex": loopCodex,
4558
4789
  "pre push": prePush,
4559
4790
  help
4560
4791
  };
@@ -4621,6 +4852,7 @@ function createFileSystem(primitives) {
4621
4852
  mkdir: async (path, options) => {
4622
4853
  await primitives.mkdir(path, options);
4623
4854
  },
4855
+ getFileCreationTime: (path) => primitives.statSync(path).birthtimeMs,
4624
4856
  readdir: (path) => primitives.readdir(path),
4625
4857
  chmod: (path, mode) => primitives.chmod(path, mode)
4626
4858
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {