@joshski/dust 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/types.d.ts +1 -0
- package/dist/dust.js +2920 -1609
- package/dist/workflow-tasks.d.ts +5 -0
- package/dist/workflow-tasks.js +46 -6
- package/package.json +1 -1
- package/templates/agent-implement-task.txt +1 -1
- package/templates/agent-new-task.txt +1 -2
- package/templates/audits/dead-code.md +30 -0
- package/templates/audits/security-review.md +30 -0
- package/templates/audits/test-coverage.md +29 -0
- package/templates/templates/agent-implement-task.txt +1 -1
- package/templates/templates/agent-new-task.txt +1 -2
- package/templates/templates/audits/dead-code.md +30 -0
- package/templates/templates/audits/security-review.md +30 -0
- package/templates/templates/audits/test-coverage.md +29 -0
package/dist/dust.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
|
+
|
|
2
5
|
// lib/cli/run.ts
|
|
3
6
|
import { existsSync } from "node:fs";
|
|
4
7
|
import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -6,7 +9,8 @@ import { chmod, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
6
9
|
// lib/config/settings.ts
|
|
7
10
|
import { join } from "node:path";
|
|
8
11
|
var DEFAULT_SETTINGS = {
|
|
9
|
-
dustCommand: "npx dust"
|
|
12
|
+
dustCommand: "npx dust",
|
|
13
|
+
installCommand: "npm install"
|
|
10
14
|
};
|
|
11
15
|
function detectDustCommand(cwd, fileSystem) {
|
|
12
16
|
if (fileSystem.exists(join(cwd, "bun.lockb"))) {
|
|
@@ -23,6 +27,21 @@ function detectDustCommand(cwd, fileSystem) {
|
|
|
23
27
|
}
|
|
24
28
|
return "npx dust";
|
|
25
29
|
}
|
|
30
|
+
function detectInstallCommand(cwd, fileSystem) {
|
|
31
|
+
if (fileSystem.exists(join(cwd, "bun.lockb"))) {
|
|
32
|
+
return "bun install";
|
|
33
|
+
}
|
|
34
|
+
if (fileSystem.exists(join(cwd, "pnpm-lock.yaml"))) {
|
|
35
|
+
return "pnpm install";
|
|
36
|
+
}
|
|
37
|
+
if (fileSystem.exists(join(cwd, "package-lock.json"))) {
|
|
38
|
+
return "npm install";
|
|
39
|
+
}
|
|
40
|
+
if (process.env.BUN_INSTALL) {
|
|
41
|
+
return "bun install";
|
|
42
|
+
}
|
|
43
|
+
return "npm install";
|
|
44
|
+
}
|
|
26
45
|
function detectTestCommand(cwd, fileSystem) {
|
|
27
46
|
if (fileSystem.exists(join(cwd, "bun.lockb")) || fileSystem.exists(join(cwd, "bun.lock"))) {
|
|
28
47
|
return "bun test";
|
|
@@ -54,7 +73,8 @@ async function loadSettings(cwd, fileSystem) {
|
|
|
54
73
|
const settingsPath = join(cwd, ".dust", "config", "settings.json");
|
|
55
74
|
if (!fileSystem.exists(settingsPath)) {
|
|
56
75
|
const result = {
|
|
57
|
-
dustCommand: detectDustCommand(cwd, fileSystem)
|
|
76
|
+
dustCommand: detectDustCommand(cwd, fileSystem),
|
|
77
|
+
installCommand: detectInstallCommand(cwd, fileSystem)
|
|
58
78
|
};
|
|
59
79
|
if (process.env.DUST_EVENTS_URL) {
|
|
60
80
|
result.eventsUrl = process.env.DUST_EVENTS_URL;
|
|
@@ -74,13 +94,17 @@ async function loadSettings(cwd, fileSystem) {
|
|
|
74
94
|
if (!parsed.dustCommand) {
|
|
75
95
|
result.dustCommand = detectDustCommand(cwd, fileSystem);
|
|
76
96
|
}
|
|
97
|
+
if (!parsed.installCommand) {
|
|
98
|
+
result.installCommand = detectInstallCommand(cwd, fileSystem);
|
|
99
|
+
}
|
|
77
100
|
if (process.env.DUST_EVENTS_URL) {
|
|
78
101
|
result.eventsUrl = process.env.DUST_EVENTS_URL;
|
|
79
102
|
}
|
|
80
103
|
return result;
|
|
81
104
|
} catch {
|
|
82
105
|
const result = {
|
|
83
|
-
dustCommand: detectDustCommand(cwd, fileSystem)
|
|
106
|
+
dustCommand: detectDustCommand(cwd, fileSystem),
|
|
107
|
+
installCommand: detectInstallCommand(cwd, fileSystem)
|
|
84
108
|
};
|
|
85
109
|
if (process.env.DUST_EVENTS_URL) {
|
|
86
110
|
result.eventsUrl = process.env.DUST_EVENTS_URL;
|
|
@@ -123,7 +147,7 @@ import { join as join4 } from "node:path";
|
|
|
123
147
|
// lib/agents/detection.ts
|
|
124
148
|
function detectAgent(env = process.env) {
|
|
125
149
|
if (env.CLAUDECODE) {
|
|
126
|
-
if (env.
|
|
150
|
+
if (env.CLAUDE_CODE_REMOTE) {
|
|
127
151
|
return { type: "claude-code-web", name: "Claude Code Web" };
|
|
128
152
|
}
|
|
129
153
|
return { type: "claude-code", name: "Claude Code" };
|
|
@@ -323,62 +347,10 @@ async function agent(dependencies) {
|
|
|
323
347
|
return { exitCode: 0 };
|
|
324
348
|
}
|
|
325
349
|
|
|
326
|
-
// lib/cli/
|
|
327
|
-
import {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
run: (command, cwd, timeoutMs) => runBufferedProcess(spawnFn, command, [], cwd, true, timeoutMs)
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
var defaultShellRunner = createShellRunner(spawn);
|
|
334
|
-
function createGitRunner(spawnFn) {
|
|
335
|
-
return {
|
|
336
|
-
run: (gitArguments, cwd) => runBufferedProcess(spawnFn, "git", gitArguments, cwd, false)
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
var defaultGitRunner = createGitRunner(spawn);
|
|
340
|
-
function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, timeoutMs) {
|
|
341
|
-
return new Promise((resolve) => {
|
|
342
|
-
const proc = spawnFn(command, commandArguments, { cwd, shell });
|
|
343
|
-
const chunks = [];
|
|
344
|
-
let resolved = false;
|
|
345
|
-
let timer;
|
|
346
|
-
if (timeoutMs !== undefined) {
|
|
347
|
-
timer = setTimeout(() => {
|
|
348
|
-
resolved = true;
|
|
349
|
-
proc.kill();
|
|
350
|
-
resolve({
|
|
351
|
-
exitCode: 1,
|
|
352
|
-
output: chunks.join(""),
|
|
353
|
-
timedOut: true
|
|
354
|
-
});
|
|
355
|
-
}, timeoutMs);
|
|
356
|
-
}
|
|
357
|
-
proc.stdout?.on("data", (data) => {
|
|
358
|
-
chunks.push(data.toString());
|
|
359
|
-
});
|
|
360
|
-
proc.stderr?.on("data", (data) => {
|
|
361
|
-
chunks.push(data.toString());
|
|
362
|
-
});
|
|
363
|
-
proc.on("close", (code) => {
|
|
364
|
-
if (resolved)
|
|
365
|
-
return;
|
|
366
|
-
if (timer !== undefined)
|
|
367
|
-
clearTimeout(timer);
|
|
368
|
-
resolve({ exitCode: code ?? 1, output: chunks.join("") });
|
|
369
|
-
});
|
|
370
|
-
proc.on("error", (error) => {
|
|
371
|
-
if (resolved)
|
|
372
|
-
return;
|
|
373
|
-
if (timer !== undefined)
|
|
374
|
-
clearTimeout(timer);
|
|
375
|
-
resolve({ exitCode: 1, output: error.message });
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// lib/cli/commands/lint-markdown.ts
|
|
381
|
-
import { dirname as dirname2, resolve } from "node:path";
|
|
350
|
+
// lib/cli/commands/audit.ts
|
|
351
|
+
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
352
|
+
import { basename, dirname as dirname2, join as join5 } from "node:path";
|
|
353
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
382
354
|
|
|
383
355
|
// lib/markdown/markdown-utilities.ts
|
|
384
356
|
function extractTitle(content) {
|
|
@@ -432,1759 +404,3096 @@ function extractOpeningSentence(content) {
|
|
|
432
404
|
return sentenceMatch[1];
|
|
433
405
|
}
|
|
434
406
|
|
|
435
|
-
// lib/
|
|
436
|
-
var
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
407
|
+
// lib/cli/colors.ts
|
|
408
|
+
var ANSI_COLORS = {
|
|
409
|
+
reset: "\x1B[0m",
|
|
410
|
+
bold: "\x1B[1m",
|
|
411
|
+
dim: "\x1B[2m",
|
|
412
|
+
cyan: "\x1B[36m",
|
|
413
|
+
green: "\x1B[32m",
|
|
414
|
+
yellow: "\x1B[33m"
|
|
415
|
+
};
|
|
416
|
+
var NO_COLORS = {
|
|
417
|
+
reset: "",
|
|
418
|
+
bold: "",
|
|
419
|
+
dim: "",
|
|
420
|
+
cyan: "",
|
|
421
|
+
green: "",
|
|
422
|
+
yellow: ""
|
|
423
|
+
};
|
|
424
|
+
function shouldDisableColors() {
|
|
425
|
+
if (process.env.NO_COLOR !== undefined) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (process.env.TERM === "dumb") {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
if (!process.stdout.isTTY) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
function getColors() {
|
|
437
|
+
return shouldDisableColors() ? NO_COLORS : ANSI_COLORS;
|
|
443
438
|
}
|
|
444
439
|
|
|
445
|
-
// lib/cli/commands/
|
|
446
|
-
var
|
|
447
|
-
var
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
440
|
+
// lib/cli/commands/audit.ts
|
|
441
|
+
var __dirname3 = dirname2(fileURLToPath2(import.meta.url));
|
|
442
|
+
var stockAuditsDir = join5(__dirname3, "../../templates/audits");
|
|
443
|
+
function loadStockAudits() {
|
|
444
|
+
const files = readdirSync(stockAuditsDir).filter((f) => f.endsWith(".md")).sort();
|
|
445
|
+
return files.map((file) => {
|
|
446
|
+
const template = readFileSync2(join5(stockAuditsDir, file), "utf-8");
|
|
447
|
+
const name = basename(file, ".md");
|
|
448
|
+
const description = extractOpeningSentence(template);
|
|
449
|
+
return { name, description, template };
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function transformAuditContent(content) {
|
|
453
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
454
|
+
if (!titleMatch) {
|
|
455
|
+
return content;
|
|
458
456
|
}
|
|
459
|
-
|
|
457
|
+
const originalTitle = titleMatch[1];
|
|
458
|
+
return content.replace(/^#\s+.+$/m, `# Audit: ${originalTitle}`);
|
|
460
459
|
}
|
|
461
|
-
function
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
460
|
+
async function addAudit(auditName, dependencies) {
|
|
461
|
+
const { context, fileSystem, settings } = dependencies;
|
|
462
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
463
|
+
const userAuditsPath = `${dustPath}/config/audits`;
|
|
464
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
465
|
+
const taskFilePath = `${tasksPath}/audit-${auditName}.md`;
|
|
466
|
+
const relativeTaskPath = `.dust/tasks/audit-${auditName}.md`;
|
|
467
|
+
if (fileSystem.exists(taskFilePath)) {
|
|
468
|
+
context.stderr(`Error: Audit task already exists at ${relativeTaskPath}`);
|
|
469
|
+
return { exitCode: 1 };
|
|
465
470
|
}
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
};
|
|
471
|
+
const userAuditPath = `${userAuditsPath}/${auditName}.md`;
|
|
472
|
+
if (fileSystem.exists(userAuditPath)) {
|
|
473
|
+
const content = await fileSystem.readFile(userAuditPath);
|
|
474
|
+
const transformedContent = transformAuditContent(content);
|
|
475
|
+
await fileSystem.mkdir(tasksPath, { recursive: true });
|
|
476
|
+
await fileSystem.writeFile(taskFilePath, transformedContent);
|
|
477
|
+
context.stdout(`→ ${relativeTaskPath}`);
|
|
478
|
+
return { exitCode: 0 };
|
|
474
479
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
message: "Missing or malformed opening sentence after H1 heading"
|
|
483
|
-
};
|
|
480
|
+
const stockAudit = loadStockAudits().find((a) => a.name === auditName);
|
|
481
|
+
if (stockAudit) {
|
|
482
|
+
const transformedContent = transformAuditContent(stockAudit.template);
|
|
483
|
+
await fileSystem.mkdir(tasksPath, { recursive: true });
|
|
484
|
+
await fileSystem.writeFile(taskFilePath, transformedContent);
|
|
485
|
+
context.stdout(`→ ${relativeTaskPath}`);
|
|
486
|
+
return { exitCode: 0 };
|
|
484
487
|
}
|
|
485
|
-
|
|
488
|
+
context.stderr(`Error: Audit '${auditName}' not found`);
|
|
489
|
+
context.stderr(`Run '${settings.dustCommand} audit' to see available audits`);
|
|
490
|
+
return { exitCode: 1 };
|
|
486
491
|
}
|
|
487
|
-
function
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
async function listAudits(dependencies) {
|
|
493
|
+
const { context, fileSystem } = dependencies;
|
|
494
|
+
const colors = getColors();
|
|
495
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
496
|
+
const userAuditsPath = `${dustPath}/config/audits`;
|
|
497
|
+
const audits = new Map;
|
|
498
|
+
for (const stockAudit of loadStockAudits()) {
|
|
499
|
+
audits.set(stockAudit.name, {
|
|
500
|
+
name: stockAudit.name,
|
|
501
|
+
description: stockAudit.description,
|
|
502
|
+
source: "stock"
|
|
503
|
+
});
|
|
491
504
|
}
|
|
492
|
-
if (
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
505
|
+
if (fileSystem.exists(userAuditsPath)) {
|
|
506
|
+
const files = await fileSystem.readdir(userAuditsPath);
|
|
507
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
508
|
+
for (const file of mdFiles) {
|
|
509
|
+
const name = basename(file, ".md");
|
|
510
|
+
const filePath = `${userAuditsPath}/${file}`;
|
|
511
|
+
const content = await fileSystem.readFile(filePath);
|
|
512
|
+
const title = extractTitle(content);
|
|
513
|
+
const openingSentence = extractOpeningSentence(content);
|
|
514
|
+
const relativePath = `.dust/config/audits/${file}`;
|
|
515
|
+
audits.set(name, {
|
|
516
|
+
name: title || name,
|
|
517
|
+
description: openingSentence || "",
|
|
518
|
+
source: relativePath
|
|
519
|
+
});
|
|
520
|
+
}
|
|
497
521
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
"
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
"it",
|
|
510
|
-
"they",
|
|
511
|
-
"you",
|
|
512
|
-
"i"
|
|
513
|
-
]);
|
|
514
|
-
function validateImperativeOpeningSentence(filePath, content) {
|
|
515
|
-
const openingSentence = extractOpeningSentence(content);
|
|
516
|
-
if (!openingSentence) {
|
|
517
|
-
return null;
|
|
522
|
+
context.stdout("\uD83D\uDD0D Audits");
|
|
523
|
+
context.stdout("");
|
|
524
|
+
context.stdout("Audits are canned tasks that help maintain project health.");
|
|
525
|
+
context.stdout("");
|
|
526
|
+
for (const auditInfo of audits.values()) {
|
|
527
|
+
context.stdout(`${colors.bold}# ${auditInfo.name}${colors.reset}`);
|
|
528
|
+
if (auditInfo.description) {
|
|
529
|
+
context.stdout(`${colors.dim}${auditInfo.description}${colors.reset}`);
|
|
530
|
+
}
|
|
531
|
+
context.stdout(`${colors.cyan}→ ${auditInfo.source}${colors.reset}`);
|
|
532
|
+
context.stdout("");
|
|
518
533
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
|
|
526
|
-
};
|
|
534
|
+
return { exitCode: 0 };
|
|
535
|
+
}
|
|
536
|
+
async function audit(dependencies) {
|
|
537
|
+
const auditName = dependencies.arguments[0];
|
|
538
|
+
if (auditName) {
|
|
539
|
+
return addAudit(auditName, dependencies);
|
|
527
540
|
}
|
|
528
|
-
return
|
|
541
|
+
return listAudits(dependencies);
|
|
529
542
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
543
|
+
|
|
544
|
+
// lib/cli/commands/bucket.ts
|
|
545
|
+
import { spawn as nodeSpawn3 } from "node:child_process";
|
|
546
|
+
import { homedir, tmpdir } from "node:os";
|
|
547
|
+
|
|
548
|
+
// lib/bucket/auth.ts
|
|
549
|
+
import { join as join6 } from "node:path";
|
|
550
|
+
var CREDENTIALS_DIR = ".dust";
|
|
551
|
+
var CREDENTIALS_FILE = "credentials.json";
|
|
552
|
+
var AUTH_TIMEOUT_MS = 120000;
|
|
553
|
+
var DEFAULT_DUSTBUCKET_HOST = "https://dustbucket.com";
|
|
554
|
+
function getDustbucketHost() {
|
|
555
|
+
return process.env.DUST_BUCKET_HOST || DEFAULT_DUSTBUCKET_HOST;
|
|
556
|
+
}
|
|
557
|
+
function credentialsPath(homeDir) {
|
|
558
|
+
return join6(homeDir, CREDENTIALS_DIR, CREDENTIALS_FILE);
|
|
559
|
+
}
|
|
560
|
+
async function loadStoredToken(fileSystem, homeDir) {
|
|
561
|
+
const path = credentialsPath(homeDir);
|
|
562
|
+
try {
|
|
563
|
+
const content = await fileSystem.readFile(path);
|
|
564
|
+
const data = JSON.parse(content);
|
|
565
|
+
return typeof data.token === "string" ? data.token : null;
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
539
568
|
}
|
|
540
|
-
return violations;
|
|
541
569
|
}
|
|
542
|
-
function
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
570
|
+
async function storeToken(fileSystem, homeDir, token) {
|
|
571
|
+
const dirPath = join6(homeDir, CREDENTIALS_DIR);
|
|
572
|
+
await fileSystem.mkdir(dirPath, { recursive: true });
|
|
573
|
+
await fileSystem.writeFile(credentialsPath(homeDir), JSON.stringify({ token }));
|
|
574
|
+
}
|
|
575
|
+
async function clearToken(fileSystem, homeDir) {
|
|
576
|
+
const path = credentialsPath(homeDir);
|
|
577
|
+
try {
|
|
578
|
+
await fileSystem.writeFile(path, "{}");
|
|
579
|
+
} catch {}
|
|
580
|
+
}
|
|
581
|
+
async function authenticate(authDeps) {
|
|
582
|
+
return new Promise((resolve, reject) => {
|
|
583
|
+
let timer = null;
|
|
584
|
+
let serverHandle = null;
|
|
585
|
+
const cleanup = () => {
|
|
586
|
+
if (timer) {
|
|
587
|
+
clearTimeout(timer);
|
|
588
|
+
timer = null;
|
|
589
|
+
}
|
|
590
|
+
if (serverHandle) {
|
|
591
|
+
serverHandle.stop();
|
|
592
|
+
serverHandle = null;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const handler = (request) => {
|
|
596
|
+
const url = new URL(request.url);
|
|
597
|
+
if (url.pathname === "/callback") {
|
|
598
|
+
const token = url.searchParams.get("token");
|
|
599
|
+
if (token) {
|
|
600
|
+
cleanup();
|
|
601
|
+
resolve(token);
|
|
602
|
+
return new Response("<html><body><p>Authentication successful! You can close this tab.</p></body></html>", { headers: { "Content-Type": "text/html" } });
|
|
562
603
|
}
|
|
604
|
+
return new Response("Missing token", { status: 400 });
|
|
563
605
|
}
|
|
564
|
-
|
|
606
|
+
return new Response("Not found", { status: 404 });
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
const server = authDeps.createServer(handler);
|
|
610
|
+
serverHandle = server;
|
|
611
|
+
const dustbucketUrl = getDustbucketHost();
|
|
612
|
+
const authUrl = `${dustbucketUrl}/auth/cli?port=${server.port}`;
|
|
613
|
+
authDeps.openBrowser(authUrl);
|
|
614
|
+
timer = setTimeout(() => {
|
|
615
|
+
cleanup();
|
|
616
|
+
reject(new Error("Authentication timed out"));
|
|
617
|
+
}, authDeps.authTimeoutMs ?? AUTH_TIMEOUT_MS);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
cleanup();
|
|
620
|
+
reject(error);
|
|
565
621
|
}
|
|
566
|
-
}
|
|
567
|
-
return violations;
|
|
622
|
+
});
|
|
568
623
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
|
|
594
|
-
violations.push({
|
|
595
|
-
file: filePath,
|
|
596
|
-
message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
|
|
597
|
-
line: i + 1
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
inOpenQuestions = line === "## Open Questions";
|
|
601
|
-
currentQuestionLine = null;
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
if (!inOpenQuestions)
|
|
605
|
-
continue;
|
|
606
|
-
if (/^[-*] /.test(line.trimStart())) {
|
|
607
|
-
violations.push({
|
|
608
|
-
file: filePath,
|
|
609
|
-
message: "Open Questions must use ### headings for questions and #### headings for options, not bullet points. Run `dust new idea` to see the expected format.",
|
|
610
|
-
line: i + 1
|
|
611
|
-
});
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (line.startsWith("### ")) {
|
|
615
|
-
if (currentQuestionLine !== null) {
|
|
616
|
-
violations.push({
|
|
617
|
-
file: filePath,
|
|
618
|
-
message: "Question has no options listed beneath it",
|
|
619
|
-
line: currentQuestionLine
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
if (!line.trimEnd().endsWith("?")) {
|
|
623
|
-
violations.push({
|
|
624
|
-
file: filePath,
|
|
625
|
-
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
626
|
-
line: i + 1
|
|
627
|
-
});
|
|
628
|
-
currentQuestionLine = null;
|
|
629
|
-
} else {
|
|
630
|
-
currentQuestionLine = i + 1;
|
|
631
|
-
}
|
|
632
|
-
continue;
|
|
633
|
-
}
|
|
634
|
-
if (line.startsWith("#### ")) {
|
|
635
|
-
currentQuestionLine = null;
|
|
624
|
+
|
|
625
|
+
// lib/bucket/events.ts
|
|
626
|
+
var WS_OPEN = 1;
|
|
627
|
+
function formatBucketEvent(event) {
|
|
628
|
+
switch (event.type) {
|
|
629
|
+
case "bucket.connected":
|
|
630
|
+
return "Connected to dustbucket";
|
|
631
|
+
case "bucket.disconnected":
|
|
632
|
+
return `Disconnected (code: ${event.code}, reason: ${event.reason || "none"})`;
|
|
633
|
+
case "bucket.repository_added":
|
|
634
|
+
return `Added repository: ${event.repository}`;
|
|
635
|
+
case "bucket.repository_removed":
|
|
636
|
+
return `Removed repository: ${event.repository}`;
|
|
637
|
+
case "bucket.error":
|
|
638
|
+
return event.repository ? `Error for ${event.repository}: ${event.error}` : `Error: ${event.error}`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function createEventMessageSender(getWebSocket) {
|
|
642
|
+
return (msg) => {
|
|
643
|
+
const ws = getWebSocket();
|
|
644
|
+
if (ws && ws.readyState === WS_OPEN) {
|
|
645
|
+
try {
|
|
646
|
+
ws.send(JSON.stringify(msg));
|
|
647
|
+
} catch {}
|
|
636
648
|
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// lib/bucket/log-buffer.ts
|
|
653
|
+
var MAX_LINES = 5000;
|
|
654
|
+
var TRIM_TO_LINES = 3000;
|
|
655
|
+
function createLogBuffer(maxLines = MAX_LINES, trimToLines = TRIM_TO_LINES) {
|
|
656
|
+
return {
|
|
657
|
+
lines: [],
|
|
658
|
+
maxLines,
|
|
659
|
+
trimToLines
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function appendLogLine(buffer, line) {
|
|
663
|
+
buffer.lines.push(line);
|
|
664
|
+
if (buffer.lines.length > buffer.maxLines) {
|
|
665
|
+
buffer.lines = buffer.lines.slice(-buffer.trimToLines);
|
|
637
666
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
667
|
+
}
|
|
668
|
+
function createLogLine(text, stream, timestamp = Date.now()) {
|
|
669
|
+
return { text, stream, timestamp };
|
|
670
|
+
}
|
|
671
|
+
function getLogLines(buffer) {
|
|
672
|
+
return buffer.lines;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// lib/bucket/repository.ts
|
|
676
|
+
import { join as join7 } from "node:path";
|
|
677
|
+
|
|
678
|
+
// lib/agent-events.ts
|
|
679
|
+
function mapToAgentEvent(event) {
|
|
680
|
+
switch (event.type) {
|
|
681
|
+
case "claude.started":
|
|
682
|
+
return { type: "agent-session-started" };
|
|
683
|
+
case "claude.ended":
|
|
684
|
+
return {
|
|
685
|
+
type: "agent-session-ended",
|
|
686
|
+
success: event.success,
|
|
687
|
+
error: event.error
|
|
688
|
+
};
|
|
689
|
+
case "claude.raw_event":
|
|
690
|
+
if (typeof event.rawEvent.type === "string" && event.rawEvent.type === "stream_event") {
|
|
691
|
+
return { type: "agent-session-activity" };
|
|
692
|
+
}
|
|
693
|
+
return { type: "claude-event", rawEvent: event.rawEvent };
|
|
694
|
+
default:
|
|
695
|
+
return null;
|
|
644
696
|
}
|
|
645
|
-
return violations;
|
|
646
697
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
698
|
+
|
|
699
|
+
// lib/claude/spawn-claude-code.ts
|
|
700
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
701
|
+
import { createInterface as nodeCreateInterface } from "node:readline";
|
|
702
|
+
var defaultDependencies = {
|
|
703
|
+
spawn: nodeSpawn,
|
|
704
|
+
createInterface: nodeCreateInterface
|
|
705
|
+
};
|
|
706
|
+
async function* spawnClaudeCode(prompt, options = {}, dependencies = defaultDependencies) {
|
|
707
|
+
const {
|
|
708
|
+
cwd,
|
|
709
|
+
allowedTools,
|
|
710
|
+
maxTurns,
|
|
711
|
+
model,
|
|
712
|
+
systemPrompt,
|
|
713
|
+
sessionId,
|
|
714
|
+
dangerouslySkipPermissions,
|
|
715
|
+
env
|
|
716
|
+
} = options;
|
|
717
|
+
const claudeArguments = [
|
|
718
|
+
"-p",
|
|
719
|
+
prompt,
|
|
720
|
+
"--output-format",
|
|
721
|
+
"stream-json",
|
|
722
|
+
"--verbose",
|
|
723
|
+
"--include-partial-messages"
|
|
724
|
+
];
|
|
725
|
+
if (allowedTools?.length) {
|
|
726
|
+
claudeArguments.push("--allowedTools", ...allowedTools);
|
|
657
727
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
728
|
+
if (maxTurns) {
|
|
729
|
+
claudeArguments.push("--max-turns", String(maxTurns));
|
|
730
|
+
}
|
|
731
|
+
if (model) {
|
|
732
|
+
claudeArguments.push("--model", model);
|
|
733
|
+
}
|
|
734
|
+
if (systemPrompt) {
|
|
735
|
+
claudeArguments.push("--system-prompt", systemPrompt);
|
|
736
|
+
}
|
|
737
|
+
if (sessionId) {
|
|
738
|
+
claudeArguments.push("--session-id", sessionId);
|
|
739
|
+
}
|
|
740
|
+
if (dangerouslySkipPermissions) {
|
|
741
|
+
claudeArguments.push("--dangerously-skip-permissions");
|
|
742
|
+
}
|
|
743
|
+
const proc = dependencies.spawn("claude", claudeArguments, {
|
|
744
|
+
cwd,
|
|
745
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
746
|
+
env: { ...process.env, ...env }
|
|
747
|
+
});
|
|
748
|
+
if (!proc.stdout) {
|
|
749
|
+
throw new Error("Failed to get stdout from claude process");
|
|
750
|
+
}
|
|
751
|
+
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
752
|
+
for await (const line of rl) {
|
|
753
|
+
if (!line.trim())
|
|
673
754
|
continue;
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
755
|
+
try {
|
|
756
|
+
yield JSON.parse(line);
|
|
757
|
+
} catch {}
|
|
758
|
+
}
|
|
759
|
+
let stderrOutput = "";
|
|
760
|
+
proc.stderr?.on("data", (data) => {
|
|
761
|
+
stderrOutput += data.toString();
|
|
762
|
+
});
|
|
763
|
+
await new Promise((resolve, reject) => {
|
|
764
|
+
proc.on("close", (code) => {
|
|
765
|
+
if (code === 0 || code === null)
|
|
766
|
+
resolve();
|
|
767
|
+
else {
|
|
768
|
+
const errMsg = stderrOutput.trim() ? `claude exited with code ${code}: ${stderrOutput.trim()}` : `claude exited with code ${code}`;
|
|
769
|
+
reject(new Error(errMsg));
|
|
686
770
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
771
|
+
});
|
|
772
|
+
proc.on("error", reject);
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// lib/claude/event-parser.ts
|
|
777
|
+
function* parseRawEvent(raw) {
|
|
778
|
+
if (raw.type === "stream_event") {
|
|
779
|
+
const event = raw.event;
|
|
780
|
+
if (event?.delta?.type === "text_delta" && event.delta.text) {
|
|
781
|
+
yield { type: "text_delta", text: event.delta.text };
|
|
782
|
+
}
|
|
783
|
+
} else if (raw.type === "assistant") {
|
|
784
|
+
const msg = raw;
|
|
785
|
+
const content = msg.message?.content ?? [];
|
|
786
|
+
yield { type: "assistant_message", content };
|
|
787
|
+
for (const block of content) {
|
|
788
|
+
if (block.type === "tool_use" && block.id && block.name && block.input) {
|
|
789
|
+
yield {
|
|
790
|
+
type: "tool_use",
|
|
791
|
+
id: block.id,
|
|
792
|
+
name: block.name,
|
|
793
|
+
input: block.input
|
|
794
|
+
};
|
|
695
795
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
796
|
+
}
|
|
797
|
+
} else if (raw.type === "user") {
|
|
798
|
+
const msg = raw;
|
|
799
|
+
for (const block of msg.message?.content ?? []) {
|
|
800
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
801
|
+
yield {
|
|
802
|
+
type: "tool_result",
|
|
803
|
+
toolUseId: block.tool_use_id,
|
|
804
|
+
content: typeof block.content === "string" ? block.content : JSON.stringify(block.content)
|
|
805
|
+
};
|
|
704
806
|
}
|
|
705
|
-
match = linkPattern.exec(line);
|
|
706
807
|
}
|
|
808
|
+
} else if (raw.type === "result") {
|
|
809
|
+
const r = raw;
|
|
810
|
+
yield {
|
|
811
|
+
type: "result",
|
|
812
|
+
subtype: r.subtype ?? "success",
|
|
813
|
+
result: r.result,
|
|
814
|
+
error: r.error,
|
|
815
|
+
cost_usd: r.total_cost_usd ?? r.cost_usd ?? 0,
|
|
816
|
+
duration_ms: r.duration_ms ?? 0,
|
|
817
|
+
num_turns: r.num_turns ?? 0,
|
|
818
|
+
session_id: r.session_id ?? ""
|
|
819
|
+
};
|
|
707
820
|
}
|
|
708
|
-
return violations;
|
|
709
821
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
822
|
+
|
|
823
|
+
// lib/claude/tool-formatters.ts
|
|
824
|
+
var DIVIDER = "────────────────────────────────";
|
|
825
|
+
function formatWrite(input) {
|
|
826
|
+
const filePath = input.file_path;
|
|
827
|
+
const content = input.content;
|
|
828
|
+
const others = getUnrecognizedArgs(input, ["file_path", "content"]);
|
|
829
|
+
const lines = [];
|
|
830
|
+
lines.push(`\uD83D\uDD27 Write: ${filePath ?? "(unknown)"}`);
|
|
831
|
+
lines.push(DIVIDER);
|
|
832
|
+
if (content !== undefined) {
|
|
833
|
+
for (const line of content.split(`
|
|
834
|
+
`)) {
|
|
835
|
+
lines.push(line);
|
|
836
|
+
}
|
|
714
837
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
838
|
+
lines.push(DIVIDER);
|
|
839
|
+
appendOtherArgs(lines, others);
|
|
840
|
+
return lines;
|
|
841
|
+
}
|
|
842
|
+
function formatEdit(input) {
|
|
843
|
+
const filePath = input.file_path;
|
|
844
|
+
const oldString = input.old_string;
|
|
845
|
+
const newString = input.new_string;
|
|
846
|
+
const others = getUnrecognizedArgs(input, [
|
|
847
|
+
"file_path",
|
|
848
|
+
"old_string",
|
|
849
|
+
"new_string",
|
|
850
|
+
"replace_all"
|
|
851
|
+
]);
|
|
852
|
+
const lines = [];
|
|
853
|
+
lines.push(`\uD83D\uDD27 Edit: ${filePath ?? "(unknown)"}`);
|
|
854
|
+
lines.push("Replace:");
|
|
855
|
+
lines.push(DIVIDER);
|
|
856
|
+
if (oldString !== undefined) {
|
|
857
|
+
for (const line of oldString.split(`
|
|
858
|
+
`)) {
|
|
859
|
+
lines.push(line);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
lines.push(DIVIDER);
|
|
863
|
+
lines.push("With:");
|
|
864
|
+
lines.push(DIVIDER);
|
|
865
|
+
if (newString !== undefined) {
|
|
866
|
+
for (const line of newString.split(`
|
|
867
|
+
`)) {
|
|
868
|
+
lines.push(line);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
lines.push(DIVIDER);
|
|
872
|
+
appendOtherArgs(lines, others);
|
|
873
|
+
return lines;
|
|
874
|
+
}
|
|
875
|
+
function formatRead(input) {
|
|
876
|
+
const filePath = input.file_path;
|
|
877
|
+
const offset = input.offset;
|
|
878
|
+
const limit = input.limit;
|
|
879
|
+
const others = getUnrecognizedArgs(input, ["file_path", "offset", "limit"]);
|
|
880
|
+
const lines = [];
|
|
881
|
+
let lineRange = "";
|
|
882
|
+
if (offset !== undefined || limit !== undefined) {
|
|
883
|
+
const start = offset ?? 1;
|
|
884
|
+
const end = limit !== undefined ? start + limit - 1 : undefined;
|
|
885
|
+
lineRange = end !== undefined ? ` (lines ${start}-${end})` : ` (from line ${start})`;
|
|
886
|
+
}
|
|
887
|
+
lines.push(`\uD83D\uDD27 Read: ${filePath ?? "(unknown)"}${lineRange}`);
|
|
888
|
+
appendOtherArgs(lines, others);
|
|
889
|
+
return lines;
|
|
890
|
+
}
|
|
891
|
+
function formatBash(input) {
|
|
892
|
+
const command = input.command;
|
|
893
|
+
const description = input.description;
|
|
894
|
+
const others = getUnrecognizedArgs(input, [
|
|
895
|
+
"command",
|
|
896
|
+
"description",
|
|
897
|
+
"timeout",
|
|
898
|
+
"run_in_background",
|
|
899
|
+
"dangerouslyDisableSandbox",
|
|
900
|
+
"_simulatedSedEdit"
|
|
901
|
+
]);
|
|
902
|
+
const lines = [];
|
|
903
|
+
const header = description ?? "Run command";
|
|
904
|
+
lines.push(`\uD83D\uDD27 Bash: ${header}`);
|
|
905
|
+
if (command !== undefined) {
|
|
906
|
+
lines.push(`$ ${command}`);
|
|
907
|
+
}
|
|
908
|
+
appendOtherArgs(lines, others);
|
|
909
|
+
return lines;
|
|
910
|
+
}
|
|
911
|
+
function formatTodoWrite(input) {
|
|
912
|
+
const todos = input.todos;
|
|
913
|
+
const others = getUnrecognizedArgs(input, ["todos"]);
|
|
914
|
+
const lines = [];
|
|
915
|
+
const count = todos?.length ?? 0;
|
|
916
|
+
lines.push(`\uD83D\uDD27 TodoWrite: ${count} item${count === 1 ? "" : "s"}`);
|
|
917
|
+
if (todos) {
|
|
918
|
+
for (const todo of todos) {
|
|
919
|
+
const icon = todo.status === "completed" ? "☑" : "☐";
|
|
920
|
+
lines.push(`${icon} ${todo.content}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
appendOtherArgs(lines, others);
|
|
924
|
+
return lines;
|
|
925
|
+
}
|
|
926
|
+
function formatGrep(input) {
|
|
927
|
+
const pattern = input.pattern;
|
|
928
|
+
const path = input.path;
|
|
929
|
+
const glob = input.glob;
|
|
930
|
+
const type = input.type;
|
|
931
|
+
const others = getUnrecognizedArgs(input, [
|
|
932
|
+
"pattern",
|
|
933
|
+
"path",
|
|
934
|
+
"glob",
|
|
935
|
+
"type",
|
|
936
|
+
"output_mode",
|
|
937
|
+
"context",
|
|
938
|
+
"-A",
|
|
939
|
+
"-B",
|
|
940
|
+
"-C",
|
|
941
|
+
"-i",
|
|
942
|
+
"-n",
|
|
943
|
+
"head_limit",
|
|
944
|
+
"offset",
|
|
945
|
+
"multiline"
|
|
946
|
+
]);
|
|
947
|
+
const lines = [];
|
|
948
|
+
const location = path ?? ".";
|
|
949
|
+
let filter = "";
|
|
950
|
+
if (glob) {
|
|
951
|
+
filter = ` (${glob})`;
|
|
952
|
+
} else if (type) {
|
|
953
|
+
filter = ` (type: ${type})`;
|
|
954
|
+
}
|
|
955
|
+
lines.push(`\uD83D\uDD27 Grep: "${pattern ?? ""}" in ${location}${filter}`);
|
|
956
|
+
appendOtherArgs(lines, others);
|
|
957
|
+
return lines;
|
|
958
|
+
}
|
|
959
|
+
function formatGlob(input) {
|
|
960
|
+
const pattern = input.pattern;
|
|
961
|
+
const path = input.path;
|
|
962
|
+
const others = getUnrecognizedArgs(input, ["pattern", "path"]);
|
|
963
|
+
const lines = [];
|
|
964
|
+
const location = path ?? ".";
|
|
965
|
+
lines.push(`\uD83D\uDD27 Glob: ${pattern ?? ""} in ${location}`);
|
|
966
|
+
appendOtherArgs(lines, others);
|
|
967
|
+
return lines;
|
|
968
|
+
}
|
|
969
|
+
function formatTask(input) {
|
|
970
|
+
const description = input.description;
|
|
971
|
+
const subagentType = input.subagent_type;
|
|
972
|
+
const prompt = input.prompt;
|
|
973
|
+
const others = getUnrecognizedArgs(input, [
|
|
974
|
+
"description",
|
|
975
|
+
"subagent_type",
|
|
976
|
+
"prompt",
|
|
977
|
+
"model",
|
|
978
|
+
"max_turns",
|
|
979
|
+
"resume",
|
|
980
|
+
"run_in_background"
|
|
981
|
+
]);
|
|
982
|
+
const lines = [];
|
|
983
|
+
const header = description ?? subagentType ?? "task";
|
|
984
|
+
lines.push(`\uD83D\uDD27 Task: ${header}`);
|
|
985
|
+
if (prompt !== undefined) {
|
|
986
|
+
const truncated = prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
|
|
987
|
+
lines.push(`"${truncated}"`);
|
|
988
|
+
}
|
|
989
|
+
appendOtherArgs(lines, others);
|
|
990
|
+
return lines;
|
|
991
|
+
}
|
|
992
|
+
function formatFallback(name, input) {
|
|
993
|
+
const lines = [];
|
|
994
|
+
lines.push(`\uD83D\uDD27 Tool: ${name}`);
|
|
995
|
+
lines.push(`Input: ${JSON.stringify(input, null, 2)}`);
|
|
996
|
+
return lines;
|
|
997
|
+
}
|
|
998
|
+
var formatters = {
|
|
999
|
+
Write: formatWrite,
|
|
1000
|
+
Edit: formatEdit,
|
|
1001
|
+
Read: formatRead,
|
|
1002
|
+
Bash: formatBash,
|
|
1003
|
+
TodoWrite: formatTodoWrite,
|
|
1004
|
+
Grep: formatGrep,
|
|
1005
|
+
Glob: formatGlob,
|
|
1006
|
+
Task: formatTask
|
|
1007
|
+
};
|
|
1008
|
+
function formatToolUse(name, input) {
|
|
1009
|
+
const formatter = formatters[name];
|
|
1010
|
+
if (formatter) {
|
|
1011
|
+
return formatter(input);
|
|
1012
|
+
}
|
|
1013
|
+
return formatFallback(name, input);
|
|
1014
|
+
}
|
|
1015
|
+
function getUnrecognizedArgs(input, knownKeys) {
|
|
1016
|
+
const others = {};
|
|
1017
|
+
for (const key of Object.keys(input)) {
|
|
1018
|
+
if (!knownKeys.includes(key)) {
|
|
1019
|
+
others[key] = input[key];
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return others;
|
|
1023
|
+
}
|
|
1024
|
+
function appendOtherArgs(lines, others) {
|
|
1025
|
+
if (Object.keys(others).length > 0) {
|
|
1026
|
+
lines.push("");
|
|
1027
|
+
lines.push(`(Other arguments: ${JSON.stringify(others)})`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// lib/claude/streamer.ts
|
|
1032
|
+
var DIVIDER2 = "────────────────────────────────";
|
|
1033
|
+
async function streamEvents(events, sink, onRawEvent) {
|
|
1034
|
+
let hadTextOutput = false;
|
|
1035
|
+
for await (const raw of events) {
|
|
1036
|
+
onRawEvent?.(raw);
|
|
1037
|
+
for (const event of parseRawEvent(raw)) {
|
|
1038
|
+
processEvent(event, sink, { hadTextOutput });
|
|
1039
|
+
if (event.type === "text_delta") {
|
|
1040
|
+
hadTextOutput = true;
|
|
1041
|
+
} else if (event.type === "tool_use") {
|
|
1042
|
+
hadTextOutput = false;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function processEvent(event, sink, state) {
|
|
1048
|
+
switch (event.type) {
|
|
1049
|
+
case "text_delta":
|
|
1050
|
+
sink.write(event.text);
|
|
1051
|
+
break;
|
|
1052
|
+
case "tool_use":
|
|
1053
|
+
if (state.hadTextOutput) {
|
|
1054
|
+
sink.line("");
|
|
1055
|
+
sink.line("");
|
|
724
1056
|
}
|
|
1057
|
+
for (const line of formatToolUse(event.name, event.input)) {
|
|
1058
|
+
sink.line(line);
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
case "tool_result":
|
|
1062
|
+
sink.line("Result:");
|
|
1063
|
+
sink.line(DIVIDER2);
|
|
1064
|
+
sink.line(event.content);
|
|
1065
|
+
sink.line(DIVIDER2);
|
|
1066
|
+
sink.line("");
|
|
1067
|
+
break;
|
|
1068
|
+
case "result":
|
|
1069
|
+
sink.line("");
|
|
1070
|
+
sink.line(`\uD83C\uDFC1 Done: ${event.subtype}, ${event.num_turns} turns, $${event.cost_usd.toFixed(4)}`);
|
|
1071
|
+
break;
|
|
1072
|
+
case "assistant_message":
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
function createStdoutSink() {
|
|
1077
|
+
return {
|
|
1078
|
+
write: (text) => process.stdout.write(text),
|
|
1079
|
+
line: (text) => console.log(text)
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// lib/claude/run.ts
|
|
1084
|
+
var defaultRunnerDependencies = {
|
|
1085
|
+
spawnClaudeCode,
|
|
1086
|
+
createStdoutSink,
|
|
1087
|
+
streamEvents
|
|
1088
|
+
};
|
|
1089
|
+
async function run(prompt, options = {}, dependencies = defaultRunnerDependencies) {
|
|
1090
|
+
const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
|
|
1091
|
+
const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
|
|
1092
|
+
const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
|
|
1093
|
+
const events = dependencies.spawnClaudeCode(prompt, spawnOptions);
|
|
1094
|
+
const sink = dependencies.createStdoutSink();
|
|
1095
|
+
await dependencies.streamEvents(events, sink, onRawEvent);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// lib/cli/commands/loop.ts
|
|
1099
|
+
import { spawn as nodeSpawn2 } from "node:child_process";
|
|
1100
|
+
|
|
1101
|
+
// lib/cli/commands/next.ts
|
|
1102
|
+
function extractBlockedBy(content) {
|
|
1103
|
+
const blockedByMatch = content.match(/^## Blocked By\s*\n([\s\S]*?)(?=\n## |\n*$)/m);
|
|
1104
|
+
if (!blockedByMatch) {
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
const section = blockedByMatch[1].trim();
|
|
1108
|
+
if (section === "(none)") {
|
|
1109
|
+
return [];
|
|
1110
|
+
}
|
|
1111
|
+
const linkPattern = /\[.*?\]\(([^)]+\.md)\)/g;
|
|
1112
|
+
const blockers = [];
|
|
1113
|
+
let match = linkPattern.exec(section);
|
|
1114
|
+
while (match !== null) {
|
|
1115
|
+
blockers.push(match[1]);
|
|
1116
|
+
match = linkPattern.exec(section);
|
|
1117
|
+
}
|
|
1118
|
+
return blockers;
|
|
1119
|
+
}
|
|
1120
|
+
async function findUnblockedTasks(cwd, fileSystem) {
|
|
1121
|
+
const dustPath = `${cwd}/.dust`;
|
|
1122
|
+
if (!fileSystem.exists(dustPath)) {
|
|
1123
|
+
return { error: ".dust directory not found", tasks: [] };
|
|
1124
|
+
}
|
|
1125
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
1126
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
1127
|
+
return { tasks: [] };
|
|
1128
|
+
}
|
|
1129
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
1130
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
1131
|
+
if (mdFiles.length === 0) {
|
|
1132
|
+
return { tasks: [] };
|
|
1133
|
+
}
|
|
1134
|
+
const existingTasks = new Set(mdFiles);
|
|
1135
|
+
const tasks = [];
|
|
1136
|
+
for (const file of mdFiles) {
|
|
1137
|
+
const filePath = `${tasksPath}/${file}`;
|
|
1138
|
+
const content = await fileSystem.readFile(filePath);
|
|
1139
|
+
const blockers = extractBlockedBy(content);
|
|
1140
|
+
const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
|
|
1141
|
+
if (!hasIncompleteBlocker) {
|
|
1142
|
+
const title = extractTitle(content);
|
|
1143
|
+
const openingSentence = extractOpeningSentence(content);
|
|
1144
|
+
const relativePath = `.dust/tasks/${file}`;
|
|
1145
|
+
tasks.push({ path: relativePath, title, openingSentence });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
return { tasks };
|
|
1149
|
+
}
|
|
1150
|
+
function printTaskList(context, tasks) {
|
|
1151
|
+
const colors = getColors();
|
|
1152
|
+
context.stdout("\uD83D\uDCCB Next tasks");
|
|
1153
|
+
context.stdout("");
|
|
1154
|
+
for (const task of tasks) {
|
|
1155
|
+
const parts = task.path.split("/");
|
|
1156
|
+
const displayTitle = task.title || parts[parts.length - 1].replace(".md", "");
|
|
1157
|
+
context.stdout(`${colors.bold}# ${displayTitle}${colors.reset}`);
|
|
1158
|
+
if (task.openingSentence) {
|
|
1159
|
+
context.stdout(`${colors.dim}${task.openingSentence}${colors.reset}`);
|
|
1160
|
+
}
|
|
1161
|
+
context.stdout(`${colors.cyan}→ ${task.path}${colors.reset}`);
|
|
1162
|
+
context.stdout("");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
async function next(dependencies) {
|
|
1166
|
+
const { context, fileSystem } = dependencies;
|
|
1167
|
+
const result = await findUnblockedTasks(context.cwd, fileSystem);
|
|
1168
|
+
if (result.error) {
|
|
1169
|
+
context.stderr(`Error: ${result.error}`);
|
|
1170
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
1171
|
+
return { exitCode: 1 };
|
|
1172
|
+
}
|
|
1173
|
+
if (result.tasks.length === 0) {
|
|
1174
|
+
return { exitCode: 0 };
|
|
1175
|
+
}
|
|
1176
|
+
printTaskList(context, result.tasks);
|
|
1177
|
+
return { exitCode: 0 };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// lib/cli/commands/loop.ts
|
|
1181
|
+
function formatEvent(event) {
|
|
1182
|
+
switch (event.type) {
|
|
1183
|
+
case "loop.warning":
|
|
1184
|
+
return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
|
|
1185
|
+
case "loop.started":
|
|
1186
|
+
return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
|
|
1187
|
+
case "loop.syncing":
|
|
1188
|
+
return "\uD83C\uDF0D Syncing with remote";
|
|
1189
|
+
case "loop.sync_skipped":
|
|
1190
|
+
return `Note: git pull skipped (${event.reason})`;
|
|
1191
|
+
case "loop.checking_tasks":
|
|
1192
|
+
return null;
|
|
1193
|
+
case "loop.no_tasks":
|
|
1194
|
+
return `\uD83D\uDE34 No tasks available. Sleeping...
|
|
1195
|
+
`;
|
|
1196
|
+
case "loop.tasks_found":
|
|
1197
|
+
return `✨ Found a task. Going to work!
|
|
1198
|
+
`;
|
|
1199
|
+
case "claude.started":
|
|
1200
|
+
return "\uD83E\uDD16 Starting Claude...";
|
|
1201
|
+
case "claude.ended":
|
|
1202
|
+
return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
|
|
1203
|
+
case "claude.raw_event":
|
|
725
1204
|
return null;
|
|
1205
|
+
case "loop.iteration_complete":
|
|
1206
|
+
return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
|
|
1207
|
+
case "loop.ended":
|
|
1208
|
+
return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
async function defaultPostEvent(url, payload) {
|
|
1212
|
+
await fetch(url, {
|
|
1213
|
+
method: "POST",
|
|
1214
|
+
headers: { "Content-Type": "application/json" },
|
|
1215
|
+
body: JSON.stringify(payload)
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
function createDefaultDependencies() {
|
|
1219
|
+
return {
|
|
1220
|
+
spawn: nodeSpawn2,
|
|
1221
|
+
run,
|
|
1222
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
1223
|
+
postEvent: defaultPostEvent
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
function createEventPoster(eventsUrl, sessionId, postEvent, onError, getAgentSessionId, repository = "") {
|
|
1227
|
+
let sequence = 0;
|
|
1228
|
+
return (event) => {
|
|
1229
|
+
if (!eventsUrl)
|
|
1230
|
+
return;
|
|
1231
|
+
const agentEvent = mapToAgentEvent(event);
|
|
1232
|
+
if (!agentEvent)
|
|
1233
|
+
return;
|
|
1234
|
+
sequence++;
|
|
1235
|
+
const payload = {
|
|
1236
|
+
sequence,
|
|
1237
|
+
timestamp: new Date().toISOString(),
|
|
1238
|
+
sessionId,
|
|
1239
|
+
repository,
|
|
1240
|
+
event: agentEvent
|
|
1241
|
+
};
|
|
1242
|
+
const agentSessionId = getAgentSessionId?.();
|
|
1243
|
+
if (agentSessionId) {
|
|
1244
|
+
payload.agentSessionId = agentSessionId;
|
|
1245
|
+
}
|
|
1246
|
+
postEvent(eventsUrl, payload).catch(onError);
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
var SLEEP_INTERVAL_MS = 30000;
|
|
1250
|
+
var DEFAULT_MAX_ITERATIONS = 10;
|
|
1251
|
+
async function gitPull(cwd, spawn) {
|
|
1252
|
+
return new Promise((resolve) => {
|
|
1253
|
+
const proc = spawn("git", ["pull"], {
|
|
1254
|
+
cwd,
|
|
1255
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1256
|
+
});
|
|
1257
|
+
let stderr = "";
|
|
1258
|
+
proc.stderr?.on("data", (data) => {
|
|
1259
|
+
stderr += data.toString();
|
|
1260
|
+
});
|
|
1261
|
+
proc.on("close", (code) => {
|
|
1262
|
+
if (code === 0) {
|
|
1263
|
+
resolve({ success: true });
|
|
1264
|
+
} else {
|
|
1265
|
+
resolve({ success: false, message: stderr.trim() || "git pull failed" });
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
proc.on("error", (error) => {
|
|
1269
|
+
resolve({ success: false, message: error.message });
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
async function hasAvailableTasks(dependencies) {
|
|
1274
|
+
let hasOutput = false;
|
|
1275
|
+
const captureContext = {
|
|
1276
|
+
...dependencies.context,
|
|
1277
|
+
stdout: () => {
|
|
1278
|
+
hasOutput = true;
|
|
726
1279
|
}
|
|
727
|
-
}
|
|
728
|
-
|
|
1280
|
+
};
|
|
1281
|
+
await next({ ...dependencies, context: captureContext });
|
|
1282
|
+
return hasOutput;
|
|
729
1283
|
}
|
|
730
|
-
function
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1284
|
+
async function runOneIteration(dependencies, loopDependencies, emit, options = {}) {
|
|
1285
|
+
const { context } = dependencies;
|
|
1286
|
+
const { spawn, run: run2 } = loopDependencies;
|
|
1287
|
+
const { onRawEvent } = options;
|
|
1288
|
+
emit({ type: "loop.syncing" });
|
|
1289
|
+
const pullResult = await gitPull(context.cwd, spawn);
|
|
1290
|
+
if (!pullResult.success) {
|
|
1291
|
+
emit({
|
|
1292
|
+
type: "loop.sync_skipped",
|
|
1293
|
+
reason: pullResult.message ?? "unknown error"
|
|
1294
|
+
});
|
|
1295
|
+
emit({ type: "claude.started" });
|
|
1296
|
+
const prompt2 = `git pull failed with the following error:
|
|
1297
|
+
|
|
1298
|
+
${pullResult.message}
|
|
1299
|
+
|
|
1300
|
+
Please resolve this issue. Common approaches:
|
|
1301
|
+
1. If there are merge conflicts, resolve them
|
|
1302
|
+
2. If local commits need to be rebased, use git rebase
|
|
1303
|
+
3. After resolving, commit any changes and push to remote
|
|
1304
|
+
|
|
1305
|
+
Make sure the repository is in a clean state and synced with remote before finishing.`;
|
|
1306
|
+
try {
|
|
1307
|
+
await run2(prompt2, {
|
|
1308
|
+
spawnOptions: {
|
|
1309
|
+
cwd: context.cwd,
|
|
1310
|
+
dangerouslySkipPermissions: true,
|
|
1311
|
+
env: { DUST_UNATTENDED: "1" }
|
|
1312
|
+
},
|
|
1313
|
+
onRawEvent
|
|
737
1314
|
});
|
|
1315
|
+
emit({ type: "claude.ended", success: true });
|
|
1316
|
+
return "resolved_pull_conflict";
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1319
|
+
context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
|
|
1320
|
+
emit({ type: "claude.ended", success: false, error: errorMessage });
|
|
738
1321
|
}
|
|
739
1322
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
`);
|
|
746
|
-
const fileDir = dirname2(filePath);
|
|
747
|
-
let currentSection = null;
|
|
748
|
-
for (let i = 0;i < lines.length; i++) {
|
|
749
|
-
const line = lines[i];
|
|
750
|
-
if (line.startsWith("## ")) {
|
|
751
|
-
currentSection = line;
|
|
752
|
-
continue;
|
|
753
|
-
}
|
|
754
|
-
if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
758
|
-
let match = linkPattern.exec(line);
|
|
759
|
-
while (match) {
|
|
760
|
-
const linkTarget = match[2];
|
|
761
|
-
if (linkTarget.startsWith("#")) {
|
|
762
|
-
violations.push({
|
|
763
|
-
file: filePath,
|
|
764
|
-
message: `Link in "${currentSection}" must point to a goal file, not an anchor: "${linkTarget}"`,
|
|
765
|
-
line: i + 1
|
|
766
|
-
});
|
|
767
|
-
match = linkPattern.exec(line);
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
771
|
-
violations.push({
|
|
772
|
-
file: filePath,
|
|
773
|
-
message: `Link in "${currentSection}" must point to a goal file, not an external URL: "${linkTarget}"`,
|
|
774
|
-
line: i + 1
|
|
775
|
-
});
|
|
776
|
-
match = linkPattern.exec(line);
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
const targetPath = linkTarget.split("#")[0];
|
|
780
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
781
|
-
if (!resolvedPath.includes("/.dust/goals/")) {
|
|
782
|
-
violations.push({
|
|
783
|
-
file: filePath,
|
|
784
|
-
message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
|
|
785
|
-
line: i + 1
|
|
786
|
-
});
|
|
787
|
-
}
|
|
788
|
-
match = linkPattern.exec(line);
|
|
789
|
-
}
|
|
1323
|
+
emit({ type: "loop.checking_tasks" });
|
|
1324
|
+
const hasTasks = await hasAvailableTasks(dependencies);
|
|
1325
|
+
if (!hasTasks) {
|
|
1326
|
+
emit({ type: "loop.no_tasks" });
|
|
1327
|
+
return "no_tasks";
|
|
790
1328
|
}
|
|
791
|
-
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const linkTarget = match[2];
|
|
812
|
-
if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
|
|
813
|
-
const targetPath = linkTarget.split("#")[0];
|
|
814
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
815
|
-
if (resolvedPath.includes("/.dust/goals/")) {
|
|
816
|
-
if (currentSection === "## Parent Goal") {
|
|
817
|
-
parentGoals.push(resolvedPath);
|
|
818
|
-
} else {
|
|
819
|
-
subGoals.push(resolvedPath);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
match = linkPattern.exec(line);
|
|
824
|
-
}
|
|
1329
|
+
emit({ type: "loop.tasks_found" });
|
|
1330
|
+
emit({ type: "claude.started" });
|
|
1331
|
+
const { dustCommand, installCommand = "npm install" } = dependencies.settings;
|
|
1332
|
+
const prompt = `Run \`${installCommand} && ${dustCommand} agent && ${dustCommand} pick task\` and follow the instructions.`;
|
|
1333
|
+
try {
|
|
1334
|
+
await run2(prompt, {
|
|
1335
|
+
spawnOptions: {
|
|
1336
|
+
cwd: context.cwd,
|
|
1337
|
+
dangerouslySkipPermissions: true,
|
|
1338
|
+
env: { DUST_UNATTENDED: "1" }
|
|
1339
|
+
},
|
|
1340
|
+
onRawEvent
|
|
1341
|
+
});
|
|
1342
|
+
emit({ type: "claude.ended", success: true });
|
|
1343
|
+
return "ran_claude";
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1346
|
+
context.stderr(`Claude exited with error: ${errorMessage}`);
|
|
1347
|
+
emit({ type: "claude.ended", success: false, error: errorMessage });
|
|
1348
|
+
return "claude_error";
|
|
825
1349
|
}
|
|
826
|
-
return { filePath, parentGoals, subGoals };
|
|
827
1350
|
}
|
|
828
|
-
function
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
for (const rel of allGoalRelationships) {
|
|
832
|
-
relationshipMap.set(rel.filePath, rel);
|
|
1351
|
+
function parseMaxIterations(commandArguments) {
|
|
1352
|
+
if (commandArguments.length === 0) {
|
|
1353
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
833
1354
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (parentRel && !parentRel.subGoals.includes(rel.filePath)) {
|
|
838
|
-
violations.push({
|
|
839
|
-
file: rel.filePath,
|
|
840
|
-
message: `Parent goal "${parentPath}" does not list this goal as a sub-goal`
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
for (const subGoalPath of rel.subGoals) {
|
|
845
|
-
const subGoalRel = relationshipMap.get(subGoalPath);
|
|
846
|
-
if (subGoalRel && !subGoalRel.parentGoals.includes(rel.filePath)) {
|
|
847
|
-
violations.push({
|
|
848
|
-
file: rel.filePath,
|
|
849
|
-
message: `Sub-goal "${subGoalPath}" does not list this goal as its parent`
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
}
|
|
1355
|
+
const parsed = Number.parseInt(commandArguments[0], 10);
|
|
1356
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
1357
|
+
return DEFAULT_MAX_ITERATIONS;
|
|
853
1358
|
}
|
|
854
|
-
return
|
|
1359
|
+
return parsed;
|
|
855
1360
|
}
|
|
856
|
-
function
|
|
857
|
-
const
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1361
|
+
async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
|
|
1362
|
+
const { context, settings } = dependencies;
|
|
1363
|
+
const { postEvent } = loopDependencies;
|
|
1364
|
+
const maxIterations = parseMaxIterations(dependencies.arguments);
|
|
1365
|
+
const eventsUrl = settings.eventsUrl;
|
|
1366
|
+
const sessionId = crypto.randomUUID();
|
|
1367
|
+
let agentSessionId;
|
|
1368
|
+
const postEventFn = createEventPoster(eventsUrl, sessionId, postEvent, (error) => {
|
|
1369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1370
|
+
context.stderr(`Event POST failed: ${message}`);
|
|
1371
|
+
}, () => agentSessionId);
|
|
1372
|
+
const emit = (event) => {
|
|
1373
|
+
const formatted = formatEvent(event);
|
|
1374
|
+
if (formatted !== null) {
|
|
1375
|
+
context.stdout(formatted);
|
|
1376
|
+
}
|
|
1377
|
+
postEventFn(event);
|
|
1378
|
+
};
|
|
1379
|
+
emit({ type: "loop.warning" });
|
|
1380
|
+
emit({ type: "loop.started", maxIterations });
|
|
1381
|
+
context.stdout(" Press Ctrl+C to stop");
|
|
1382
|
+
context.stdout("");
|
|
1383
|
+
let completedIterations = 0;
|
|
1384
|
+
const iterationOptions = {};
|
|
1385
|
+
if (eventsUrl) {
|
|
1386
|
+
iterationOptions.onRawEvent = (rawEvent) => {
|
|
1387
|
+
if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
|
|
1388
|
+
agentSessionId = rawEvent.session_id;
|
|
883
1389
|
}
|
|
1390
|
+
emit({ type: "claude.raw_event", rawEvent });
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
while (completedIterations < maxIterations) {
|
|
1394
|
+
agentSessionId = undefined;
|
|
1395
|
+
const result = await runOneIteration(dependencies, loopDependencies, emit, iterationOptions);
|
|
1396
|
+
if (result === "no_tasks") {
|
|
1397
|
+
await loopDependencies.sleep(SLEEP_INTERVAL_MS);
|
|
1398
|
+
} else {
|
|
1399
|
+
completedIterations++;
|
|
1400
|
+
emit({
|
|
1401
|
+
type: "loop.iteration_complete",
|
|
1402
|
+
iteration: completedIterations,
|
|
1403
|
+
maxIterations
|
|
1404
|
+
});
|
|
884
1405
|
}
|
|
885
1406
|
}
|
|
886
|
-
|
|
1407
|
+
emit({ type: "loop.ended", maxIterations });
|
|
1408
|
+
return { exitCode: 0 };
|
|
887
1409
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1410
|
+
|
|
1411
|
+
// lib/bucket/repository.ts
|
|
1412
|
+
var SLEEP_INTERVAL_MS2 = 30000;
|
|
1413
|
+
function parseRepository(data) {
|
|
1414
|
+
if (typeof data === "string") {
|
|
1415
|
+
return { name: data, gitUrl: data };
|
|
1416
|
+
}
|
|
1417
|
+
if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
|
|
1418
|
+
const repositoryData = data;
|
|
1419
|
+
if (typeof repositoryData.name === "string" && typeof repositoryData.gitUrl === "string") {
|
|
1420
|
+
return { name: repositoryData.name, gitUrl: repositoryData.gitUrl };
|
|
898
1421
|
}
|
|
899
|
-
throw error;
|
|
900
1422
|
}
|
|
1423
|
+
return null;
|
|
901
1424
|
}
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
} catch (error) {
|
|
922
|
-
if (error.code !== "ENOENT") {
|
|
923
|
-
throw error;
|
|
1425
|
+
function getRepoTempPath(repoName, tempDir) {
|
|
1426
|
+
const safeName = repoName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
1427
|
+
return join7(tempDir, `dust-bucket-${safeName}`);
|
|
1428
|
+
}
|
|
1429
|
+
async function cloneRepository(repository, targetPath, spawn, context) {
|
|
1430
|
+
return new Promise((resolve) => {
|
|
1431
|
+
const proc = spawn("git", ["clone", repository.gitUrl, targetPath], {
|
|
1432
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1433
|
+
});
|
|
1434
|
+
let stderr = "";
|
|
1435
|
+
proc.stderr?.on("data", (data) => {
|
|
1436
|
+
stderr += data.toString();
|
|
1437
|
+
});
|
|
1438
|
+
proc.on("close", (code) => {
|
|
1439
|
+
if (code === 0) {
|
|
1440
|
+
resolve(true);
|
|
1441
|
+
} else {
|
|
1442
|
+
context.stderr(`Failed to clone ${repository.name}: ${stderr.trim()}`);
|
|
1443
|
+
resolve(false);
|
|
924
1444
|
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1445
|
+
});
|
|
1446
|
+
proc.on("error", (error) => {
|
|
1447
|
+
context.stderr(`Failed to clone ${repository.name}: ${error.message}`);
|
|
1448
|
+
resolve(false);
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
async function removeRepository(path, spawn, context) {
|
|
1453
|
+
return new Promise((resolve) => {
|
|
1454
|
+
const proc = spawn("rm", ["-rf", path], {
|
|
1455
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1456
|
+
});
|
|
1457
|
+
proc.on("close", (code) => {
|
|
1458
|
+
resolve(code === 0);
|
|
1459
|
+
});
|
|
1460
|
+
proc.on("error", (error) => {
|
|
1461
|
+
context.stderr(`Failed to remove ${path}: ${error.message}`);
|
|
1462
|
+
resolve(false);
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function createNoOpGlobScanner() {
|
|
1467
|
+
return {
|
|
1468
|
+
scan: async function* () {}
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
async function runRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
|
|
1472
|
+
const { spawn, run: run2, fileSystem, sleep } = repoDeps;
|
|
1473
|
+
const repoName = repoState.repository.name;
|
|
1474
|
+
const settings = await loadSettings(repoState.path, fileSystem);
|
|
1475
|
+
const commandDeps = {
|
|
1476
|
+
arguments: [],
|
|
1477
|
+
context: {
|
|
1478
|
+
cwd: repoState.path,
|
|
1479
|
+
stdout: (msg) => appendLogLine(repoState.logBuffer, createLogLine(msg, "stdout")),
|
|
1480
|
+
stderr: (msg) => appendLogLine(repoState.logBuffer, createLogLine(msg, "stderr"))
|
|
1481
|
+
},
|
|
1482
|
+
fileSystem,
|
|
1483
|
+
globScanner: createNoOpGlobScanner(),
|
|
1484
|
+
settings
|
|
1485
|
+
};
|
|
1486
|
+
let partialLine = "";
|
|
1487
|
+
const bufferSinkDeps = {
|
|
1488
|
+
...defaultRunnerDependencies,
|
|
1489
|
+
createStdoutSink: () => ({
|
|
1490
|
+
write: (text) => {
|
|
1491
|
+
partialLine += text;
|
|
1492
|
+
const lines = partialLine.split(`
|
|
1493
|
+
`);
|
|
1494
|
+
for (let i = 0;i < lines.length - 1; i++) {
|
|
1495
|
+
appendLogLine(repoState.logBuffer, createLogLine(lines[i], "stdout"));
|
|
942
1496
|
}
|
|
943
|
-
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
const openingSentenceLengthViolation = validateOpeningSentenceLength(filePath, content);
|
|
950
|
-
if (openingSentenceLengthViolation) {
|
|
951
|
-
violations.push(openingSentenceLengthViolation);
|
|
952
|
-
}
|
|
953
|
-
const titleFilenameViolation = validateTitleFilenameMatch(filePath, content);
|
|
954
|
-
if (titleFilenameViolation) {
|
|
955
|
-
violations.push(titleFilenameViolation);
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
const ideasPath = `${dustPath}/ideas`;
|
|
960
|
-
const { files: ideaFiles } = await safeScanDir(glob, ideasPath);
|
|
961
|
-
if (ideaFiles.length > 0) {
|
|
962
|
-
context.stdout("Validating idea files in .dust/ideas/...");
|
|
963
|
-
for (const file of ideaFiles) {
|
|
964
|
-
if (!file.endsWith(".md"))
|
|
965
|
-
continue;
|
|
966
|
-
const filePath = `${ideasPath}/${file}`;
|
|
967
|
-
let content;
|
|
968
|
-
try {
|
|
969
|
-
content = await fileSystem.readFile(filePath);
|
|
970
|
-
} catch (error) {
|
|
971
|
-
if (error.code === "ENOENT") {
|
|
972
|
-
continue;
|
|
1497
|
+
partialLine = lines[lines.length - 1];
|
|
1498
|
+
},
|
|
1499
|
+
line: (text) => {
|
|
1500
|
+
if (partialLine) {
|
|
1501
|
+
appendLogLine(repoState.logBuffer, createLogLine(partialLine, "stdout"));
|
|
1502
|
+
partialLine = "";
|
|
973
1503
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
const tasksPath = `${dustPath}/tasks`;
|
|
980
|
-
const { files: taskFiles } = await safeScanDir(glob, tasksPath);
|
|
981
|
-
if (taskFiles.length > 0) {
|
|
982
|
-
context.stdout("Validating task files in .dust/tasks/...");
|
|
983
|
-
for (const file of taskFiles) {
|
|
984
|
-
if (!file.endsWith(".md"))
|
|
985
|
-
continue;
|
|
986
|
-
const filePath = `${tasksPath}/${file}`;
|
|
987
|
-
let content;
|
|
988
|
-
try {
|
|
989
|
-
content = await fileSystem.readFile(filePath);
|
|
990
|
-
} catch (error) {
|
|
991
|
-
if (error.code === "ENOENT") {
|
|
992
|
-
continue;
|
|
1504
|
+
for (const segment of text.split(`
|
|
1505
|
+
`)) {
|
|
1506
|
+
appendLogLine(repoState.logBuffer, createLogLine(segment, "stdout"));
|
|
993
1507
|
}
|
|
994
|
-
throw error;
|
|
995
|
-
}
|
|
996
|
-
const filenameViolation = validateFilename(filePath);
|
|
997
|
-
if (filenameViolation) {
|
|
998
|
-
violations.push(filenameViolation);
|
|
999
|
-
}
|
|
1000
|
-
violations.push(...validateTaskHeadings(filePath, content));
|
|
1001
|
-
violations.push(...validateSemanticLinks(filePath, content));
|
|
1002
|
-
const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
|
|
1003
|
-
if (imperativeViolation) {
|
|
1004
|
-
violations.push(imperativeViolation);
|
|
1005
1508
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1509
|
+
})
|
|
1510
|
+
};
|
|
1511
|
+
const bufferRun = (prompt, options) => run2(prompt, options, bufferSinkDeps);
|
|
1512
|
+
const loopDeps = {
|
|
1513
|
+
spawn,
|
|
1514
|
+
run: bufferRun,
|
|
1515
|
+
sleep,
|
|
1516
|
+
postEvent: async () => {}
|
|
1517
|
+
};
|
|
1518
|
+
let agentSessionId;
|
|
1519
|
+
let sequence = 0;
|
|
1520
|
+
const loopEmit = (event) => {
|
|
1521
|
+
const formatted = formatEvent(event);
|
|
1522
|
+
if (formatted !== null) {
|
|
1523
|
+
appendLogLine(repoState.logBuffer, createLogLine(formatted, "stdout"));
|
|
1524
|
+
}
|
|
1525
|
+
const agentEvent = mapToAgentEvent(event);
|
|
1526
|
+
if (agentEvent && sendEvent && sessionId) {
|
|
1527
|
+
sequence++;
|
|
1528
|
+
const msg = {
|
|
1529
|
+
sequence,
|
|
1530
|
+
timestamp: new Date().toISOString(),
|
|
1531
|
+
sessionId,
|
|
1532
|
+
repository: repoName,
|
|
1533
|
+
event: agentEvent
|
|
1534
|
+
};
|
|
1535
|
+
if (agentSessionId) {
|
|
1536
|
+
msg.agentSessionId = agentSessionId;
|
|
1009
1537
|
}
|
|
1538
|
+
sendEvent(msg);
|
|
1010
1539
|
}
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
if (!file.endsWith(".md"))
|
|
1019
|
-
continue;
|
|
1020
|
-
const filePath = `${goalsPath}/${file}`;
|
|
1021
|
-
let content;
|
|
1022
|
-
try {
|
|
1023
|
-
content = await fileSystem.readFile(filePath);
|
|
1024
|
-
} catch (error) {
|
|
1025
|
-
if (error.code === "ENOENT") {
|
|
1026
|
-
continue;
|
|
1540
|
+
};
|
|
1541
|
+
while (!repoState.stopRequested) {
|
|
1542
|
+
agentSessionId = undefined;
|
|
1543
|
+
const result = await runOneIteration(commandDeps, loopDeps, loopEmit, {
|
|
1544
|
+
onRawEvent: (rawEvent) => {
|
|
1545
|
+
if (typeof rawEvent.session_id === "string" && rawEvent.session_id) {
|
|
1546
|
+
agentSessionId = rawEvent.session_id;
|
|
1027
1547
|
}
|
|
1028
|
-
|
|
1548
|
+
loopEmit({ type: "claude.raw_event", rawEvent });
|
|
1029
1549
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1550
|
+
});
|
|
1551
|
+
if (result === "no_tasks") {
|
|
1552
|
+
await sleep(SLEEP_INTERVAL_MS2);
|
|
1033
1553
|
}
|
|
1034
|
-
violations.push(...validateBidirectionalLinks(allGoalRelationships));
|
|
1035
|
-
violations.push(...validateNoCycles(allGoalRelationships));
|
|
1036
1554
|
}
|
|
1037
|
-
|
|
1038
|
-
context.stdout("All validations passed!");
|
|
1039
|
-
return { exitCode: 0 };
|
|
1040
|
-
}
|
|
1041
|
-
context.stderr(`Found ${violations.length} violation(s):`);
|
|
1042
|
-
context.stderr("");
|
|
1043
|
-
for (const v of violations) {
|
|
1044
|
-
const location = v.line ? `:${v.line}` : "";
|
|
1045
|
-
context.stderr(` ${v.file}${location}`);
|
|
1046
|
-
context.stderr(` ${v.message}`);
|
|
1047
|
-
}
|
|
1048
|
-
return { exitCode: 1 };
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// lib/cli/commands/check.ts
|
|
1052
|
-
var DEFAULT_CHECK_TIMEOUT_MS = 13000;
|
|
1053
|
-
async function runSingleCheck(check, cwd, runner) {
|
|
1054
|
-
const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
1055
|
-
const startTime = Date.now();
|
|
1056
|
-
const result = await runner.run(check.command, cwd, timeoutMs);
|
|
1057
|
-
const durationMs = Date.now() - startTime;
|
|
1058
|
-
return {
|
|
1059
|
-
name: check.name,
|
|
1060
|
-
command: check.command,
|
|
1061
|
-
exitCode: result.exitCode,
|
|
1062
|
-
output: result.output,
|
|
1063
|
-
hints: check.hints,
|
|
1064
|
-
durationMs,
|
|
1065
|
-
timedOut: result.timedOut,
|
|
1066
|
-
timeoutSeconds: timeoutMs / 1000
|
|
1067
|
-
};
|
|
1068
|
-
}
|
|
1069
|
-
async function runConfiguredChecks(checks, cwd, runner) {
|
|
1070
|
-
const promises = checks.map((check) => runSingleCheck(check, cwd, runner));
|
|
1071
|
-
return Promise.all(promises);
|
|
1072
|
-
}
|
|
1073
|
-
async function runConfiguredChecksSerially(checks, cwd, runner) {
|
|
1074
|
-
const results = [];
|
|
1075
|
-
for (const check of checks) {
|
|
1076
|
-
results.push(await runSingleCheck(check, cwd, runner));
|
|
1077
|
-
}
|
|
1078
|
-
return results;
|
|
1555
|
+
appendLogLine(repoState.logBuffer, createLogLine(`Stopped loop for ${repoName}`, "stdout"));
|
|
1079
1556
|
}
|
|
1080
|
-
async function
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1557
|
+
async function addRepository(repository, manager, repoDeps, context) {
|
|
1558
|
+
if (manager.repositories.has(repository.name)) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const repoPath = getRepoTempPath(repository.name, repoDeps.getTempDir());
|
|
1562
|
+
if (repoDeps.fileSystem.exists(repoPath)) {
|
|
1563
|
+
await removeRepository(repoPath, repoDeps.spawn, context);
|
|
1564
|
+
}
|
|
1565
|
+
context.stdout(`Adding repository: ${repository.name}`);
|
|
1566
|
+
const success = await cloneRepository(repository, repoPath, repoDeps.spawn, context);
|
|
1567
|
+
if (!success) {
|
|
1568
|
+
const errorEvent = {
|
|
1569
|
+
type: "bucket.error",
|
|
1570
|
+
repository: repository.name,
|
|
1571
|
+
error: "Clone failed"
|
|
1572
|
+
};
|
|
1573
|
+
manager.emit(errorEvent);
|
|
1574
|
+
context.stderr(formatBucketEvent(errorEvent));
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const repoState = {
|
|
1578
|
+
repository,
|
|
1579
|
+
path: repoPath,
|
|
1580
|
+
loopPromise: null,
|
|
1581
|
+
stopRequested: false,
|
|
1582
|
+
logBuffer: manager.logBuffers.get(repository.name) ?? createLogBuffer()
|
|
1583
|
+
};
|
|
1584
|
+
manager.repositories.set(repository.name, repoState);
|
|
1585
|
+
const addedEvent = {
|
|
1586
|
+
type: "bucket.repository_added",
|
|
1587
|
+
repository: repository.name
|
|
1102
1588
|
};
|
|
1589
|
+
manager.emit(addedEvent);
|
|
1590
|
+
context.stdout(formatBucketEvent(addedEvent));
|
|
1591
|
+
repoState.loopPromise = runRepositoryLoop(repoState, repoDeps, manager.sendEvent, manager.sessionId);
|
|
1592
|
+
}
|
|
1593
|
+
async function removeRepositoryFromManager(repoName, manager, repoDeps, context) {
|
|
1594
|
+
const repoState = manager.repositories.get(repoName);
|
|
1595
|
+
if (!repoState) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
repoState.stopRequested = true;
|
|
1599
|
+
if (repoState.loopPromise) {
|
|
1600
|
+
await Promise.race([repoState.loopPromise, repoDeps.sleep(5000)]);
|
|
1601
|
+
}
|
|
1602
|
+
await removeRepository(repoState.path, repoDeps.spawn, context);
|
|
1603
|
+
manager.repositories.delete(repoName);
|
|
1604
|
+
const removedEvent = {
|
|
1605
|
+
type: "bucket.repository_removed",
|
|
1606
|
+
repository: repoName
|
|
1607
|
+
};
|
|
1608
|
+
manager.emit(removedEvent);
|
|
1609
|
+
context.stdout(formatBucketEvent(removedEvent));
|
|
1103
1610
|
}
|
|
1104
|
-
function
|
|
1105
|
-
const
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
if (
|
|
1109
|
-
|
|
1110
|
-
} else {
|
|
1111
|
-
const timing = result.durationMs !== undefined && result.durationMs >= 1000 ? ` [${(result.durationMs / 1000).toFixed(1)}s]` : "";
|
|
1112
|
-
if (result.exitCode === 0) {
|
|
1113
|
-
context.stdout(`✓ ${result.name}${timing}`);
|
|
1114
|
-
} else {
|
|
1115
|
-
context.stdout(`✗ ${result.name}${timing}`);
|
|
1116
|
-
}
|
|
1611
|
+
async function handleRepositoryList(repositories, manager, repoDeps, context) {
|
|
1612
|
+
const incomingRepos = new Map;
|
|
1613
|
+
for (const data of repositories) {
|
|
1614
|
+
const repo = parseRepository(data);
|
|
1615
|
+
if (repo) {
|
|
1616
|
+
incomingRepos.set(repo.name, repo);
|
|
1117
1617
|
}
|
|
1118
1618
|
}
|
|
1119
|
-
for (const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if (result.timedOut) {
|
|
1123
|
-
context.stdout(`Note: This check was killed after ${result.timeoutSeconds}s. To configure a different timeout, set "timeoutMilliseconds" in the check configuration in .dust/config/settings.json`);
|
|
1619
|
+
for (const [name, repo] of incomingRepos) {
|
|
1620
|
+
if (!manager.repositories.has(name)) {
|
|
1621
|
+
await addRepository(repo, manager, repoDeps, context);
|
|
1124
1622
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1623
|
+
}
|
|
1624
|
+
for (const name of manager.repositories.keys()) {
|
|
1625
|
+
if (!incomingRepos.has(name)) {
|
|
1626
|
+
await removeRepositoryFromManager(name, manager, repoDeps, context);
|
|
1127
1627
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// lib/bucket/terminal-ui.ts
|
|
1632
|
+
var ANSI = {
|
|
1633
|
+
HIDE_CURSOR: "\x1B[?25l",
|
|
1634
|
+
SHOW_CURSOR: "\x1B[?25h",
|
|
1635
|
+
MOVE_TO: (row, col) => `\x1B[${row};${col}H`,
|
|
1636
|
+
CLEAR_LINE: "\x1B[2K",
|
|
1637
|
+
CLEAR_SCREEN: "\x1B[2J",
|
|
1638
|
+
ENTER_ALT_SCREEN: "\x1B[?1049h",
|
|
1639
|
+
EXIT_ALT_SCREEN: "\x1B[?1049l",
|
|
1640
|
+
RESET: "\x1B[0m",
|
|
1641
|
+
BOLD: "\x1B[1m",
|
|
1642
|
+
DIM: "\x1B[2m",
|
|
1643
|
+
UNDERLINE: "\x1B[4m",
|
|
1644
|
+
INVERSE: "\x1B[7m",
|
|
1645
|
+
FG_BLACK: "\x1B[30m",
|
|
1646
|
+
FG_RED: "\x1B[31m",
|
|
1647
|
+
FG_GREEN: "\x1B[32m",
|
|
1648
|
+
FG_YELLOW: "\x1B[33m",
|
|
1649
|
+
FG_BLUE: "\x1B[34m",
|
|
1650
|
+
FG_MAGENTA: "\x1B[35m",
|
|
1651
|
+
FG_CYAN: "\x1B[36m",
|
|
1652
|
+
FG_WHITE: "\x1B[37m",
|
|
1653
|
+
BG_BLACK: "\x1B[40m",
|
|
1654
|
+
BG_RED: "\x1B[41m",
|
|
1655
|
+
BG_GREEN: "\x1B[42m",
|
|
1656
|
+
BG_YELLOW: "\x1B[43m",
|
|
1657
|
+
BG_BLUE: "\x1B[44m",
|
|
1658
|
+
BG_MAGENTA: "\x1B[45m",
|
|
1659
|
+
BG_CYAN: "\x1B[46m",
|
|
1660
|
+
BG_WHITE: "\x1B[47m"
|
|
1661
|
+
};
|
|
1662
|
+
var REPO_COLORS = [
|
|
1663
|
+
ANSI.FG_CYAN,
|
|
1664
|
+
ANSI.FG_MAGENTA,
|
|
1665
|
+
ANSI.FG_YELLOW,
|
|
1666
|
+
ANSI.FG_GREEN,
|
|
1667
|
+
ANSI.FG_BLUE,
|
|
1668
|
+
ANSI.FG_RED
|
|
1669
|
+
];
|
|
1670
|
+
var SYSTEM_COLOR = ANSI.DIM;
|
|
1671
|
+
function visibleLength(text) {
|
|
1672
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1673
|
+
}
|
|
1674
|
+
function truncateLine(text, maxWidth) {
|
|
1675
|
+
if (maxWidth <= 0)
|
|
1676
|
+
return "";
|
|
1677
|
+
if (visibleLength(text) <= maxWidth)
|
|
1678
|
+
return text;
|
|
1679
|
+
const ansiRegex = /\x1b\[[0-9;]*m/g;
|
|
1680
|
+
let visibleCount = 0;
|
|
1681
|
+
let result = "";
|
|
1682
|
+
let lastIndex = 0;
|
|
1683
|
+
for (let match = ansiRegex.exec(text);match !== null; match = ansiRegex.exec(text)) {
|
|
1684
|
+
const textBefore = text.slice(lastIndex, match.index);
|
|
1685
|
+
for (const char of textBefore) {
|
|
1686
|
+
if (visibleCount >= maxWidth - 1) {
|
|
1687
|
+
result += "…";
|
|
1688
|
+
return result + ANSI.RESET;
|
|
1133
1689
|
}
|
|
1690
|
+
result += char;
|
|
1691
|
+
visibleCount++;
|
|
1134
1692
|
}
|
|
1693
|
+
result += match[0];
|
|
1694
|
+
lastIndex = match.index + match[0].length;
|
|
1135
1695
|
}
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1696
|
+
const remaining = text.slice(lastIndex);
|
|
1697
|
+
for (const char of remaining) {
|
|
1698
|
+
if (visibleCount >= maxWidth - 1) {
|
|
1699
|
+
result += "…";
|
|
1700
|
+
return result + ANSI.RESET;
|
|
1701
|
+
}
|
|
1702
|
+
result += char;
|
|
1703
|
+
visibleCount++;
|
|
1704
|
+
}
|
|
1705
|
+
return result;
|
|
1140
1706
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1707
|
+
function createTerminalUIState() {
|
|
1708
|
+
return {
|
|
1709
|
+
repositories: [],
|
|
1710
|
+
selectedIndex: -1,
|
|
1711
|
+
logBuffers: new Map,
|
|
1712
|
+
scrollOffset: 0,
|
|
1713
|
+
autoScroll: true,
|
|
1714
|
+
width: 80,
|
|
1715
|
+
height: 24,
|
|
1716
|
+
connectedHost: ""
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function updateDimensions(state, width, height) {
|
|
1720
|
+
state.width = width;
|
|
1721
|
+
state.height = height;
|
|
1722
|
+
}
|
|
1723
|
+
function addRepository2(state, name, logBuffer) {
|
|
1724
|
+
if (!state.repositories.includes(name)) {
|
|
1725
|
+
state.repositories.push(name);
|
|
1726
|
+
state.repositories.sort((a, b) => {
|
|
1727
|
+
if (a === "system")
|
|
1728
|
+
return 1;
|
|
1729
|
+
if (b === "system")
|
|
1730
|
+
return -1;
|
|
1731
|
+
return a.localeCompare(b);
|
|
1732
|
+
});
|
|
1160
1733
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1734
|
+
state.logBuffers.set(name, logBuffer);
|
|
1735
|
+
}
|
|
1736
|
+
function removeRepository2(state, name) {
|
|
1737
|
+
const index = state.repositories.indexOf(name);
|
|
1738
|
+
if (index >= 0) {
|
|
1739
|
+
state.repositories.splice(index, 1);
|
|
1740
|
+
state.logBuffers.delete(name);
|
|
1741
|
+
if (state.selectedIndex >= state.repositories.length) {
|
|
1742
|
+
state.selectedIndex = state.repositories.length - 1;
|
|
1167
1743
|
}
|
|
1168
|
-
const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner);
|
|
1169
|
-
results2.push(...configuredResults);
|
|
1170
|
-
const exitCode2 = displayResults(results2, context);
|
|
1171
|
-
return { exitCode: exitCode2 };
|
|
1172
1744
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1745
|
+
}
|
|
1746
|
+
function selectNext(state) {
|
|
1747
|
+
if (state.repositories.length === 0)
|
|
1748
|
+
return;
|
|
1749
|
+
state.selectedIndex++;
|
|
1750
|
+
if (state.selectedIndex >= state.repositories.length) {
|
|
1751
|
+
state.selectedIndex = -1;
|
|
1176
1752
|
}
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1753
|
+
}
|
|
1754
|
+
function selectPrevious(state) {
|
|
1755
|
+
if (state.repositories.length === 0)
|
|
1756
|
+
return;
|
|
1757
|
+
state.selectedIndex--;
|
|
1758
|
+
if (state.selectedIndex < -1) {
|
|
1759
|
+
state.selectedIndex = state.repositories.length - 1;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
function scrollUp(state, lines = 1) {
|
|
1763
|
+
const logs = getVisibleLogs(state);
|
|
1764
|
+
const maxScroll = Math.max(0, logs.length - getLogAreaHeight(state));
|
|
1765
|
+
state.scrollOffset = Math.min(state.scrollOffset + lines, maxScroll);
|
|
1766
|
+
if (state.scrollOffset > 0) {
|
|
1767
|
+
state.autoScroll = false;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function scrollDown(state, lines = 1) {
|
|
1771
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - lines);
|
|
1772
|
+
if (state.scrollOffset === 0) {
|
|
1773
|
+
state.autoScroll = true;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
function scrollToTop(state) {
|
|
1777
|
+
const logs = getVisibleLogs(state);
|
|
1778
|
+
const maxScroll = Math.max(0, logs.length - getLogAreaHeight(state));
|
|
1779
|
+
state.scrollOffset = maxScroll;
|
|
1780
|
+
state.autoScroll = false;
|
|
1781
|
+
}
|
|
1782
|
+
function scrollToBottom(state) {
|
|
1783
|
+
state.scrollOffset = 0;
|
|
1784
|
+
state.autoScroll = true;
|
|
1785
|
+
}
|
|
1786
|
+
function getTabRowCount(state) {
|
|
1787
|
+
if (state.width <= 0)
|
|
1788
|
+
return 1;
|
|
1789
|
+
const tabWidths = [5];
|
|
1790
|
+
for (const name of state.repositories) {
|
|
1791
|
+
tabWidths.push(name.length + 2);
|
|
1792
|
+
}
|
|
1793
|
+
let rows = 1;
|
|
1794
|
+
let currentRowWidth = 0;
|
|
1795
|
+
for (const tabWidth of tabWidths) {
|
|
1796
|
+
const separatorWidth = currentRowWidth > 0 ? 1 : 0;
|
|
1797
|
+
if (currentRowWidth + separatorWidth + tabWidth > state.width && currentRowWidth > 0) {
|
|
1798
|
+
rows++;
|
|
1799
|
+
currentRowWidth = tabWidth;
|
|
1183
1800
|
} else {
|
|
1184
|
-
|
|
1801
|
+
currentRowWidth += separatorWidth + tabWidth;
|
|
1185
1802
|
}
|
|
1186
1803
|
}
|
|
1187
|
-
|
|
1188
|
-
return { exitCode };
|
|
1804
|
+
return rows;
|
|
1189
1805
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1806
|
+
function getLogAreaHeight(state) {
|
|
1807
|
+
const tabRows = getTabRowCount(state);
|
|
1808
|
+
return Math.max(1, state.height - (tabRows + 3));
|
|
1809
|
+
}
|
|
1810
|
+
function getRepoColor(name, index) {
|
|
1811
|
+
if (name === "system")
|
|
1812
|
+
return SYSTEM_COLOR;
|
|
1813
|
+
return REPO_COLORS[index % REPO_COLORS.length];
|
|
1814
|
+
}
|
|
1815
|
+
function getVisibleLogs(state) {
|
|
1816
|
+
if (state.selectedIndex === -1) {
|
|
1817
|
+
const allLogs = [];
|
|
1818
|
+
const repoColors = new Map;
|
|
1819
|
+
for (let i = 0;i < state.repositories.length; i++) {
|
|
1820
|
+
const repoName2 = state.repositories[i];
|
|
1821
|
+
repoColors.set(repoName2, getRepoColor(repoName2, i));
|
|
1822
|
+
}
|
|
1823
|
+
for (const repoName2 of state.repositories) {
|
|
1824
|
+
const buffer2 = state.logBuffers.get(repoName2);
|
|
1825
|
+
if (!buffer2)
|
|
1826
|
+
continue;
|
|
1827
|
+
const color2 = repoColors.get(repoName2) ?? ANSI.FG_WHITE;
|
|
1828
|
+
const lines = getLogLines(buffer2);
|
|
1829
|
+
for (const line of lines) {
|
|
1830
|
+
allLogs.push({ ...line, repository: repoName2, color: color2 });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
allLogs.sort((a, b) => a.timestamp - b.timestamp);
|
|
1834
|
+
return allLogs;
|
|
1199
1835
|
}
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1836
|
+
const repoName = state.repositories[state.selectedIndex];
|
|
1837
|
+
if (!repoName)
|
|
1838
|
+
return [];
|
|
1839
|
+
const buffer = state.logBuffers.get(repoName);
|
|
1840
|
+
if (!buffer)
|
|
1841
|
+
return [];
|
|
1842
|
+
const color = getRepoColor(repoName, state.selectedIndex);
|
|
1843
|
+
return getLogLines(buffer).map((line) => ({
|
|
1844
|
+
...line,
|
|
1845
|
+
repository: repoName,
|
|
1846
|
+
color
|
|
1847
|
+
}));
|
|
1848
|
+
}
|
|
1849
|
+
function renderTabs(state) {
|
|
1850
|
+
const tabs = [];
|
|
1851
|
+
if (state.selectedIndex === -1) {
|
|
1852
|
+
tabs.push({ text: `${ANSI.INVERSE} All ${ANSI.RESET}`, width: 5 });
|
|
1853
|
+
} else {
|
|
1854
|
+
tabs.push({ text: " All ", width: 5 });
|
|
1855
|
+
}
|
|
1856
|
+
for (let i = 0;i < state.repositories.length; i++) {
|
|
1857
|
+
const name = state.repositories[i];
|
|
1858
|
+
const color = getRepoColor(name, i);
|
|
1859
|
+
const width = name.length + 2;
|
|
1860
|
+
if (i === state.selectedIndex) {
|
|
1861
|
+
tabs.push({
|
|
1862
|
+
text: `${ANSI.INVERSE}${color} ${name} ${ANSI.RESET}`,
|
|
1863
|
+
width
|
|
1864
|
+
});
|
|
1865
|
+
} else {
|
|
1866
|
+
tabs.push({ text: `${color} ${name} ${ANSI.RESET}`, width });
|
|
1867
|
+
}
|
|
1213
1868
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1869
|
+
const rows = [[]];
|
|
1870
|
+
let currentRowWidth = 0;
|
|
1871
|
+
for (const tab of tabs) {
|
|
1872
|
+
const separatorWidth = currentRowWidth > 0 ? 1 : 0;
|
|
1873
|
+
if (currentRowWidth + separatorWidth + tab.width > state.width && currentRowWidth > 0) {
|
|
1874
|
+
rows.push([tab]);
|
|
1875
|
+
currentRowWidth = tab.width;
|
|
1876
|
+
} else {
|
|
1877
|
+
rows[rows.length - 1].push(tab);
|
|
1878
|
+
currentRowWidth += separatorWidth + tab.width;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return rows.map((row) => row.map((t) => t.text).join("|"));
|
|
1222
1882
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
function generateHelpText(settings) {
|
|
1226
|
-
return loadTemplate("help", { bin: settings.dustCommand });
|
|
1883
|
+
function renderHelpLine() {
|
|
1884
|
+
return `${ANSI.DIM}[←→] select [↑↓] scroll [PgUp/PgDn] page [g/G] top/bottom [q] quit${ANSI.RESET}`;
|
|
1227
1885
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
return { exitCode: 0 };
|
|
1886
|
+
function renderSeparator(width) {
|
|
1887
|
+
return "─".repeat(width);
|
|
1231
1888
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1889
|
+
function formatLogLine(line, prefixAlign, maxWidth) {
|
|
1890
|
+
let prefix = "";
|
|
1891
|
+
let prefixWidth = 0;
|
|
1892
|
+
if (prefixAlign > 0) {
|
|
1893
|
+
const paddedName = line.repository.padEnd(prefixAlign);
|
|
1894
|
+
prefix = `${line.color}${paddedName}${ANSI.RESET} ${ANSI.DIM}|${ANSI.RESET} `;
|
|
1895
|
+
prefixWidth = prefixAlign + 3;
|
|
1896
|
+
}
|
|
1897
|
+
const textColor = line.stream === "stderr" ? ANSI.FG_RED : "";
|
|
1898
|
+
const textReset = line.stream === "stderr" ? ANSI.RESET : "";
|
|
1899
|
+
const sanitizedText = line.text.replace(/[\r\n]+/g, "");
|
|
1900
|
+
const text = `${textColor}${sanitizedText}${textReset}`;
|
|
1901
|
+
const availableWidth = maxWidth - prefixWidth;
|
|
1902
|
+
if (availableWidth <= 0)
|
|
1903
|
+
return prefix;
|
|
1904
|
+
return prefix + truncateLine(text, availableWidth);
|
|
1240
1905
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1906
|
+
function renderFrame(state) {
|
|
1907
|
+
const lines = [];
|
|
1908
|
+
const hostLabel = state.connectedHost ? ` ${ANSI.DIM}[connected to ${state.connectedHost}]${ANSI.RESET}` : "";
|
|
1909
|
+
lines.push(`${ANSI.BOLD}✨ dust bucket${ANSI.RESET}${hostLabel}`);
|
|
1910
|
+
const tabRows = renderTabs(state);
|
|
1911
|
+
for (const tabRow of tabRows) {
|
|
1912
|
+
lines.push(tabRow);
|
|
1913
|
+
}
|
|
1914
|
+
const contentWidth = state.width - 1;
|
|
1915
|
+
lines.push(truncateLine(renderHelpLine(), contentWidth));
|
|
1916
|
+
lines.push(renderSeparator(contentWidth));
|
|
1917
|
+
const logs = getVisibleLogs(state);
|
|
1918
|
+
const logAreaHeight = getLogAreaHeight(state);
|
|
1919
|
+
const prefixAlign = state.selectedIndex === -1 ? Math.max(0, ...state.repositories.map((r) => r.length)) : 0;
|
|
1920
|
+
const totalLogs = logs.length;
|
|
1921
|
+
const endIndex = Math.max(0, totalLogs - state.scrollOffset);
|
|
1922
|
+
const startIndex = Math.max(0, endIndex - logAreaHeight);
|
|
1923
|
+
for (let i = startIndex;i < endIndex; i++) {
|
|
1924
|
+
const logLine = logs[i];
|
|
1925
|
+
lines.push(formatLogLine(logLine, prefixAlign, contentWidth));
|
|
1926
|
+
}
|
|
1927
|
+
const renderedLogLines = endIndex - startIndex;
|
|
1928
|
+
for (let i = renderedLogLines;i < logAreaHeight; i++) {
|
|
1929
|
+
lines.push("");
|
|
1930
|
+
}
|
|
1931
|
+
if (state.scrollOffset > 0) {
|
|
1932
|
+
const indicator = `${ANSI.DIM}↓ ${state.scrollOffset} more${ANSI.RESET}`;
|
|
1933
|
+
lines[lines.length - 1] = indicator;
|
|
1934
|
+
}
|
|
1935
|
+
const output = [];
|
|
1936
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1937
|
+
output.push(ANSI.MOVE_TO(i + 1, 1));
|
|
1938
|
+
output.push(ANSI.CLEAR_LINE);
|
|
1939
|
+
output.push(lines[i]);
|
|
1940
|
+
}
|
|
1941
|
+
return output.join("");
|
|
1942
|
+
}
|
|
1943
|
+
function enterAlternateScreen() {
|
|
1944
|
+
return ANSI.ENTER_ALT_SCREEN + ANSI.HIDE_CURSOR + ANSI.CLEAR_SCREEN + "\x1B[?1000h" + "\x1B[?1006h";
|
|
1945
|
+
}
|
|
1946
|
+
function exitAlternateScreen() {
|
|
1947
|
+
return `\x1B[?1006l\x1B[?1000l${ANSI.EXIT_ALT_SCREEN}${ANSI.SHOW_CURSOR}`;
|
|
1948
|
+
}
|
|
1949
|
+
var KEYS = {
|
|
1950
|
+
UP: "\x1B[A",
|
|
1951
|
+
DOWN: "\x1B[B",
|
|
1952
|
+
RIGHT: "\x1B[C",
|
|
1953
|
+
LEFT: "\x1B[D",
|
|
1954
|
+
PAGE_UP: "\x1B[5~",
|
|
1955
|
+
PAGE_DOWN: "\x1B[6~",
|
|
1956
|
+
HOME: "\x1B[H",
|
|
1957
|
+
END: "\x1B[F",
|
|
1958
|
+
CTRL_C: "\x03"
|
|
1258
1959
|
};
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1960
|
+
var SGR_MOUSE_RE = new RegExp(String.raw`^\x1b\[<(\d+);\d+;\d+[Mm]$`);
|
|
1961
|
+
function parseSGRMouse(key) {
|
|
1962
|
+
const match = key.match(SGR_MOUSE_RE);
|
|
1963
|
+
if (!match)
|
|
1964
|
+
return null;
|
|
1965
|
+
return Number.parseInt(match[1], 10);
|
|
1966
|
+
}
|
|
1967
|
+
function handleKeyInput(state, key) {
|
|
1968
|
+
const mouseButton = parseSGRMouse(key);
|
|
1969
|
+
if (mouseButton !== null) {
|
|
1970
|
+
if (mouseButton === 64) {
|
|
1971
|
+
scrollUp(state, 3);
|
|
1972
|
+
} else if (mouseButton === 65) {
|
|
1973
|
+
scrollDown(state, 3);
|
|
1974
|
+
}
|
|
1975
|
+
return false;
|
|
1262
1976
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1977
|
+
switch (key) {
|
|
1978
|
+
case "q":
|
|
1979
|
+
case KEYS.CTRL_C:
|
|
1980
|
+
return true;
|
|
1981
|
+
case KEYS.LEFT:
|
|
1982
|
+
selectPrevious(state);
|
|
1983
|
+
state.scrollOffset = 0;
|
|
1984
|
+
state.autoScroll = true;
|
|
1985
|
+
break;
|
|
1986
|
+
case KEYS.RIGHT:
|
|
1987
|
+
selectNext(state);
|
|
1988
|
+
state.scrollOffset = 0;
|
|
1989
|
+
state.autoScroll = true;
|
|
1990
|
+
break;
|
|
1991
|
+
case KEYS.UP:
|
|
1992
|
+
scrollUp(state, 1);
|
|
1993
|
+
break;
|
|
1994
|
+
case KEYS.DOWN:
|
|
1995
|
+
scrollDown(state, 1);
|
|
1996
|
+
break;
|
|
1997
|
+
case KEYS.PAGE_UP:
|
|
1998
|
+
scrollUp(state, getLogAreaHeight(state));
|
|
1999
|
+
break;
|
|
2000
|
+
case KEYS.PAGE_DOWN:
|
|
2001
|
+
scrollDown(state, getLogAreaHeight(state));
|
|
2002
|
+
break;
|
|
2003
|
+
case "g":
|
|
2004
|
+
case KEYS.HOME:
|
|
2005
|
+
scrollToTop(state);
|
|
2006
|
+
break;
|
|
2007
|
+
case "G":
|
|
2008
|
+
case KEYS.END:
|
|
2009
|
+
scrollToBottom(state);
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
// lib/cli/commands/bucket.ts
|
|
2016
|
+
var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
|
|
2017
|
+
var INITIAL_RECONNECT_DELAY_MS = 1000;
|
|
2018
|
+
var MAX_RECONNECT_DELAY_MS = 30000;
|
|
2019
|
+
function defaultCreateWebSocket(url, token) {
|
|
2020
|
+
const ws = new WebSocket(url, {
|
|
2021
|
+
headers: {
|
|
2022
|
+
Authorization: `Bearer ${token}`
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
return ws;
|
|
2026
|
+
}
|
|
2027
|
+
function defaultSetupKeypress(onKey) {
|
|
2028
|
+
const stdin = process.stdin;
|
|
2029
|
+
if (!stdin.isTTY) {
|
|
2030
|
+
return () => {};
|
|
2031
|
+
}
|
|
2032
|
+
stdin.setRawMode(true);
|
|
2033
|
+
stdin.resume();
|
|
2034
|
+
stdin.setEncoding("utf8");
|
|
2035
|
+
const handler = (key) => {
|
|
2036
|
+
onKey(key);
|
|
2037
|
+
};
|
|
2038
|
+
stdin.on("data", handler);
|
|
2039
|
+
return () => {
|
|
2040
|
+
stdin.removeListener("data", handler);
|
|
2041
|
+
stdin.setRawMode(false);
|
|
2042
|
+
stdin.pause();
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function defaultSetupSignals(onSignal) {
|
|
2046
|
+
const handler = () => onSignal();
|
|
2047
|
+
process.on("SIGINT", handler);
|
|
2048
|
+
process.on("SIGTERM", handler);
|
|
2049
|
+
return () => {
|
|
2050
|
+
process.removeListener("SIGINT", handler);
|
|
2051
|
+
process.removeListener("SIGTERM", handler);
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function defaultSetupResize(onResize) {
|
|
2055
|
+
const handler = () => {
|
|
2056
|
+
const { columns, rows } = process.stdout;
|
|
2057
|
+
onResize(columns ?? 80, rows ?? 24);
|
|
2058
|
+
};
|
|
2059
|
+
process.stdout.on("resize", handler);
|
|
2060
|
+
return () => {
|
|
2061
|
+
process.stdout.removeListener("resize", handler);
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
function defaultGetTerminalSize() {
|
|
2065
|
+
return {
|
|
2066
|
+
width: process.stdout.columns ?? 80,
|
|
2067
|
+
height: process.stdout.rows ?? 24
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
function defaultWriteStdout(data) {
|
|
2071
|
+
process.stdout.write(data);
|
|
2072
|
+
}
|
|
2073
|
+
function defaultCreateServer(handler) {
|
|
2074
|
+
const server = Bun.serve({
|
|
2075
|
+
port: 0,
|
|
2076
|
+
fetch: handler
|
|
2077
|
+
});
|
|
2078
|
+
return { port: server.port ?? 0, stop: () => server.stop() };
|
|
2079
|
+
}
|
|
2080
|
+
function defaultOpenBrowser(url) {
|
|
2081
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
2082
|
+
nodeSpawn3(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
2083
|
+
}
|
|
2084
|
+
function createDefaultBucketDependencies() {
|
|
2085
|
+
const authFileSystem = {
|
|
2086
|
+
exists: (path) => {
|
|
2087
|
+
try {
|
|
2088
|
+
const file = Bun.file(path);
|
|
2089
|
+
return file.size > 0;
|
|
2090
|
+
} catch {
|
|
2091
|
+
return false;
|
|
2092
|
+
}
|
|
2093
|
+
},
|
|
2094
|
+
readFile: (path) => Bun.file(path).text(),
|
|
2095
|
+
writeFile: (path, content) => Bun.write(path, content).then(() => {}),
|
|
2096
|
+
mkdir: (path, options) => {
|
|
2097
|
+
const { mkdir } = __require("node:fs/promises");
|
|
2098
|
+
return mkdir(path, options);
|
|
2099
|
+
},
|
|
2100
|
+
readdir: (path) => {
|
|
2101
|
+
const { readdir } = __require("node:fs/promises");
|
|
2102
|
+
return readdir(path);
|
|
2103
|
+
},
|
|
2104
|
+
chmod: (path, mode) => {
|
|
2105
|
+
const { chmod } = __require("node:fs/promises");
|
|
2106
|
+
return chmod(path, mode);
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
return {
|
|
2110
|
+
spawn: nodeSpawn3,
|
|
2111
|
+
createWebSocket: defaultCreateWebSocket,
|
|
2112
|
+
setupKeypress: defaultSetupKeypress,
|
|
2113
|
+
setupSignals: defaultSetupSignals,
|
|
2114
|
+
setupResize: defaultSetupResize,
|
|
2115
|
+
getTerminalSize: defaultGetTerminalSize,
|
|
2116
|
+
writeStdout: defaultWriteStdout,
|
|
2117
|
+
isTTY: process.stdout.isTTY ?? false,
|
|
2118
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
2119
|
+
getTempDir: () => tmpdir(),
|
|
2120
|
+
auth: {
|
|
2121
|
+
createServer: defaultCreateServer,
|
|
2122
|
+
openBrowser: defaultOpenBrowser,
|
|
2123
|
+
getHomeDir: () => homedir(),
|
|
2124
|
+
fileSystem: authFileSystem
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
function createInitialState() {
|
|
2129
|
+
const sessionId = crypto.randomUUID();
|
|
2130
|
+
const systemBuffer = createLogBuffer();
|
|
2131
|
+
const state = {
|
|
2132
|
+
ws: null,
|
|
2133
|
+
repositories: new Map,
|
|
2134
|
+
reconnectDelay: INITIAL_RECONNECT_DELAY_MS,
|
|
2135
|
+
reconnectTimer: null,
|
|
2136
|
+
shuttingDown: false,
|
|
2137
|
+
sessionId,
|
|
2138
|
+
emit: () => {},
|
|
2139
|
+
sendEvent: () => {},
|
|
2140
|
+
ui: createTerminalUIState(),
|
|
2141
|
+
logBuffers: new Map
|
|
2142
|
+
};
|
|
2143
|
+
state.sendEvent = createEventMessageSender(() => state.ws);
|
|
2144
|
+
state.logBuffers.set("system", systemBuffer);
|
|
2145
|
+
addRepository2(state.ui, "system", systemBuffer);
|
|
2146
|
+
return state;
|
|
2147
|
+
}
|
|
2148
|
+
function getWebSocketUrl() {
|
|
2149
|
+
return process.env.DUST_BUCKET_AGENT_CONNECT_URL || DEFAULT_DUSTBUCKET_WS_URL;
|
|
2150
|
+
}
|
|
2151
|
+
function toRepositoryDependencies(bucketDeps, fileSystem) {
|
|
2152
|
+
return {
|
|
2153
|
+
spawn: bucketDeps.spawn,
|
|
2154
|
+
run,
|
|
2155
|
+
fileSystem,
|
|
2156
|
+
sleep: bucketDeps.sleep,
|
|
2157
|
+
getTempDir: bucketDeps.getTempDir
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function syncUIWithRepoList(state, repos) {
|
|
2161
|
+
const incomingNames = new Set;
|
|
2162
|
+
for (const data of repos) {
|
|
2163
|
+
const repo = parseRepository(data);
|
|
2164
|
+
if (repo) {
|
|
2165
|
+
incomingNames.add(repo.name);
|
|
2166
|
+
if (!state.ui.repositories.includes(repo.name)) {
|
|
2167
|
+
let buffer = state.logBuffers.get(repo.name);
|
|
2168
|
+
if (!buffer) {
|
|
2169
|
+
buffer = createLogBuffer();
|
|
2170
|
+
state.logBuffers.set(repo.name, buffer);
|
|
2171
|
+
}
|
|
2172
|
+
addRepository2(state.ui, repo.name, buffer);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
for (const name of [...state.ui.repositories]) {
|
|
2177
|
+
if (name !== "system" && !incomingNames.has(name)) {
|
|
2178
|
+
state.logBuffers.delete(name);
|
|
2179
|
+
removeRepository2(state.ui, name);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
function syncTUI(state) {
|
|
2184
|
+
const currentUIRepos = new Set(state.ui.repositories);
|
|
2185
|
+
const currentRepos = new Set(state.repositories.keys());
|
|
2186
|
+
for (const [name, repoState] of state.repositories) {
|
|
2187
|
+
state.logBuffers.set(name, repoState.logBuffer);
|
|
2188
|
+
addRepository2(state.ui, name, repoState.logBuffer);
|
|
2189
|
+
}
|
|
2190
|
+
for (const name of currentUIRepos) {
|
|
2191
|
+
if (name !== "system" && !currentRepos.has(name)) {
|
|
2192
|
+
state.logBuffers.delete(name);
|
|
2193
|
+
removeRepository2(state.ui, name);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function logMessage(state, context, useTUI, message, stream = "stdout") {
|
|
2198
|
+
if (useTUI) {
|
|
2199
|
+
const systemBuffer = state.logBuffers.get("system");
|
|
2200
|
+
if (!systemBuffer)
|
|
2201
|
+
return;
|
|
2202
|
+
appendLogLine(systemBuffer, createLogLine(message, stream));
|
|
2203
|
+
} else if (stream === "stderr") {
|
|
2204
|
+
context.stderr(message);
|
|
2205
|
+
} else {
|
|
2206
|
+
context.stdout(message);
|
|
1265
2207
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
2208
|
+
}
|
|
2209
|
+
function createTUIContext(state, context, useTUI) {
|
|
2210
|
+
if (!useTUI)
|
|
2211
|
+
return context;
|
|
2212
|
+
return {
|
|
2213
|
+
...context,
|
|
2214
|
+
stdout: (message) => logMessage(state, context, true, message, "stdout"),
|
|
2215
|
+
stderr: (message) => logMessage(state, context, true, message, "stderr")
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
function waitForConnection(token, bucketDeps) {
|
|
2219
|
+
const wsUrl = getWebSocketUrl();
|
|
2220
|
+
const ws = bucketDeps.createWebSocket(wsUrl, token);
|
|
2221
|
+
return new Promise((resolve, reject) => {
|
|
2222
|
+
ws.onopen = () => resolve(ws);
|
|
2223
|
+
ws.onerror = (error) => reject(new Error(error.message));
|
|
2224
|
+
ws.onclose = (event) => reject(new Error(`Connection closed (code ${event.code})`));
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2227
|
+
function connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI, connectedWs) {
|
|
2228
|
+
if (state.shuttingDown)
|
|
2229
|
+
return;
|
|
2230
|
+
const wsUrl = getWebSocketUrl();
|
|
2231
|
+
let ws;
|
|
2232
|
+
if (connectedWs) {
|
|
2233
|
+
ws = connectedWs;
|
|
2234
|
+
state.ws = ws;
|
|
2235
|
+
state.emit({ type: "bucket.connected" });
|
|
2236
|
+
logMessage(state, context, useTUI, formatBucketEvent({ type: "bucket.connected" }));
|
|
2237
|
+
state.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2238
|
+
} else {
|
|
2239
|
+
logMessage(state, context, useTUI, `Connecting to ${wsUrl}...`);
|
|
2240
|
+
ws = bucketDependencies.createWebSocket(wsUrl, token);
|
|
2241
|
+
state.ws = ws;
|
|
2242
|
+
ws.onopen = () => {
|
|
2243
|
+
state.emit({ type: "bucket.connected" });
|
|
2244
|
+
logMessage(state, context, useTUI, formatBucketEvent({ type: "bucket.connected" }));
|
|
2245
|
+
state.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
2246
|
+
};
|
|
1268
2247
|
}
|
|
1269
|
-
|
|
2248
|
+
ws.onclose = (event) => {
|
|
2249
|
+
const disconnectEvent = {
|
|
2250
|
+
type: "bucket.disconnected",
|
|
2251
|
+
code: event.code,
|
|
2252
|
+
reason: event.reason || "none"
|
|
2253
|
+
};
|
|
2254
|
+
state.emit(disconnectEvent);
|
|
2255
|
+
logMessage(state, context, useTUI, formatBucketEvent(disconnectEvent));
|
|
2256
|
+
state.ws = null;
|
|
2257
|
+
if (!state.shuttingDown) {
|
|
2258
|
+
logMessage(state, context, useTUI, `Reconnecting in ${state.reconnectDelay / 1000} seconds...`);
|
|
2259
|
+
state.reconnectTimer = setTimeout(() => {
|
|
2260
|
+
connectWebSocket(token, state, bucketDependencies, context, fileSystem, useTUI);
|
|
2261
|
+
}, state.reconnectDelay);
|
|
2262
|
+
state.reconnectDelay = Math.min(state.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
ws.onerror = (error) => {
|
|
2266
|
+
logMessage(state, context, useTUI, `WebSocket error: ${error.message}`, "stderr");
|
|
2267
|
+
};
|
|
2268
|
+
ws.onmessage = (event) => {
|
|
2269
|
+
try {
|
|
2270
|
+
const message = JSON.parse(event.data);
|
|
2271
|
+
if (message.type === "repository-list") {
|
|
2272
|
+
const repos = message.repositories ?? [];
|
|
2273
|
+
logMessage(state, context, useTUI, `Received repository list (${repos.length} repositories)`);
|
|
2274
|
+
syncUIWithRepoList(state, repos);
|
|
2275
|
+
const repoDeps = toRepositoryDependencies(bucketDependencies, fileSystem);
|
|
2276
|
+
const repoContext = createTUIContext(state, context, useTUI);
|
|
2277
|
+
handleRepositoryList(repos, state, repoDeps, repoContext).then(() => syncTUI(state)).catch((error) => {
|
|
2278
|
+
logMessage(state, context, useTUI, `Failed to handle repository list: ${error.message}`, "stderr");
|
|
2279
|
+
});
|
|
2280
|
+
}
|
|
2281
|
+
} catch {
|
|
2282
|
+
logMessage(state, context, useTUI, `Failed to parse WebSocket message: ${event.data}`, "stderr");
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
1270
2285
|
}
|
|
1271
|
-
function
|
|
1272
|
-
|
|
2286
|
+
async function shutdown(state, bucketDeps, context) {
|
|
2287
|
+
if (state.shuttingDown)
|
|
2288
|
+
return;
|
|
2289
|
+
state.shuttingDown = true;
|
|
2290
|
+
context.stdout("Shutting down...");
|
|
2291
|
+
if (state.reconnectTimer) {
|
|
2292
|
+
clearTimeout(state.reconnectTimer);
|
|
2293
|
+
state.reconnectTimer = null;
|
|
2294
|
+
}
|
|
2295
|
+
if (state.ws && state.ws.readyState === WS_OPEN) {
|
|
2296
|
+
state.ws.close();
|
|
2297
|
+
state.ws = null;
|
|
2298
|
+
}
|
|
2299
|
+
for (const repoState of state.repositories.values()) {
|
|
2300
|
+
repoState.stopRequested = true;
|
|
2301
|
+
}
|
|
2302
|
+
const loopPromises = Array.from(state.repositories.values()).map((rs) => rs.loopPromise).filter((p) => p !== null);
|
|
2303
|
+
await Promise.all(loopPromises.map((p) => p.catch(() => {})));
|
|
2304
|
+
for (const repoState of state.repositories.values()) {
|
|
2305
|
+
await removeRepository(repoState.path, bucketDeps.spawn, context);
|
|
2306
|
+
}
|
|
2307
|
+
state.repositories.clear();
|
|
2308
|
+
}
|
|
2309
|
+
function setupTUI(state, bucketDeps) {
|
|
2310
|
+
const { width, height } = bucketDeps.getTerminalSize();
|
|
2311
|
+
updateDimensions(state.ui, width, height);
|
|
2312
|
+
bucketDeps.writeStdout(enterAlternateScreen());
|
|
2313
|
+
const cleanupResize = bucketDeps.setupResize((w, h) => {
|
|
2314
|
+
updateDimensions(state.ui, w, h);
|
|
2315
|
+
});
|
|
2316
|
+
const renderInterval = setInterval(() => {
|
|
2317
|
+
if (!state.shuttingDown) {
|
|
2318
|
+
bucketDeps.writeStdout(renderFrame(state.ui));
|
|
2319
|
+
}
|
|
2320
|
+
}, 100);
|
|
2321
|
+
return {
|
|
2322
|
+
cleanup: () => {
|
|
2323
|
+
clearInterval(renderInterval);
|
|
2324
|
+
bucketDeps.writeStdout(exitAlternateScreen());
|
|
2325
|
+
cleanupResize();
|
|
2326
|
+
}
|
|
2327
|
+
};
|
|
1273
2328
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (testCommand) {
|
|
1282
|
-
checks.push({ name: "test", command: testCommand });
|
|
2329
|
+
function createKeypressHandler(useTUI, state, onQuit) {
|
|
2330
|
+
if (useTUI) {
|
|
2331
|
+
return (key) => {
|
|
2332
|
+
const shouldQuit = handleKeyInput(state.ui, key);
|
|
2333
|
+
if (shouldQuit)
|
|
2334
|
+
onQuit();
|
|
2335
|
+
};
|
|
1283
2336
|
}
|
|
1284
|
-
return
|
|
2337
|
+
return (key) => {
|
|
2338
|
+
if (key === "q" || key === "\x03")
|
|
2339
|
+
onQuit();
|
|
2340
|
+
};
|
|
1285
2341
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
`;
|
|
1290
|
-
async function init(dependencies) {
|
|
1291
|
-
const { context, fileSystem } = dependencies;
|
|
1292
|
-
const colors = getColors();
|
|
1293
|
-
const dustPath = `${context.cwd}/.dust`;
|
|
1294
|
-
const dustCommand = detectDustCommand(context.cwd, fileSystem);
|
|
1295
|
-
const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
|
|
1296
|
-
await fileSystem.mkdir(dustPath, { recursive: true });
|
|
1297
|
-
for (const dir of DUST_DIRECTORIES) {
|
|
1298
|
-
await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
|
|
2342
|
+
async function resolveToken(commandArgs, authDeps, context) {
|
|
2343
|
+
if (commandArgs[0]) {
|
|
2344
|
+
return commandArgs[0];
|
|
1299
2345
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
dustDirCreated = true;
|
|
1304
|
-
} catch (error) {
|
|
1305
|
-
if (error.code !== "EEXIST") {
|
|
1306
|
-
throw error;
|
|
1307
|
-
}
|
|
2346
|
+
const stored = await loadStoredToken(authDeps.fileSystem, authDeps.getHomeDir());
|
|
2347
|
+
if (stored) {
|
|
2348
|
+
return stored;
|
|
1308
2349
|
}
|
|
2350
|
+
context.stdout("Opening browser to authenticate with dustbucket...");
|
|
1309
2351
|
try {
|
|
1310
|
-
const
|
|
1311
|
-
await
|
|
1312
|
-
|
|
2352
|
+
const token = await authenticate(authDeps);
|
|
2353
|
+
await storeToken(authDeps.fileSystem, authDeps.getHomeDir(), token);
|
|
2354
|
+
context.stdout("Authenticated successfully");
|
|
2355
|
+
return token;
|
|
1313
2356
|
} catch (error) {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
}
|
|
2357
|
+
context.stderr(`Authentication failed: ${error.message}`);
|
|
2358
|
+
return null;
|
|
1317
2359
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
context.stdout(`${colors.yellow}\uD83D\uDCE6 Note:${colors.reset} ${colors.cyan}.dust${colors.reset} directory already exists, skipping creation`);
|
|
2360
|
+
}
|
|
2361
|
+
async function bucket(dependencies, bucketDeps = createDefaultBucketDependencies()) {
|
|
2362
|
+
const { arguments: commandArgs, context, fileSystem } = dependencies;
|
|
2363
|
+
const token = await resolveToken(commandArgs, bucketDeps.auth, context);
|
|
2364
|
+
if (!token) {
|
|
2365
|
+
return { exitCode: 1 };
|
|
1325
2366
|
}
|
|
1326
|
-
const
|
|
2367
|
+
const wsUrl = getWebSocketUrl();
|
|
2368
|
+
context.stdout(`Connecting to ${wsUrl}...`);
|
|
2369
|
+
let initialWs;
|
|
1327
2370
|
try {
|
|
1328
|
-
|
|
1329
|
-
await fileSystem.writeFile(claudeMdPath, claudeContent, { flag: "wx" });
|
|
1330
|
-
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
|
|
2371
|
+
initialWs = await waitForConnection(token, bucketDeps);
|
|
1331
2372
|
} catch (error) {
|
|
1332
|
-
if (error.
|
|
1333
|
-
context.
|
|
2373
|
+
if (error.message.includes("1008") || error.message.includes("401")) {
|
|
2374
|
+
context.stderr("Token rejected. Clearing stored credentials...");
|
|
2375
|
+
await clearToken(bucketDeps.auth.fileSystem, bucketDeps.auth.getHomeDir());
|
|
2376
|
+
context.stderr("Run `dust bucket` again to re-authenticate.");
|
|
1334
2377
|
} else {
|
|
1335
|
-
|
|
2378
|
+
context.stderr(`Failed to connect: ${error.message}`);
|
|
1336
2379
|
}
|
|
2380
|
+
return { exitCode: 1 };
|
|
1337
2381
|
}
|
|
1338
|
-
|
|
2382
|
+
context.stdout("Connected");
|
|
2383
|
+
const state = createInitialState();
|
|
2384
|
+
const useTUI = bucketDeps.isTTY;
|
|
1339
2385
|
try {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
if (error.code === "EEXIST") {
|
|
1345
|
-
context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
|
|
1346
|
-
} else {
|
|
1347
|
-
throw error;
|
|
1348
|
-
}
|
|
2386
|
+
state.ui.connectedHost = new URL(wsUrl).hostname;
|
|
2387
|
+
} catch {
|
|
2388
|
+
state.ui.connectedHost = wsUrl;
|
|
1349
2389
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
2390
|
+
let tuiHandle;
|
|
2391
|
+
let cleanupKeypress;
|
|
2392
|
+
let cleanupSignals;
|
|
2393
|
+
try {
|
|
2394
|
+
if (useTUI) {
|
|
2395
|
+
tuiHandle = setupTUI(state, bucketDeps);
|
|
2396
|
+
}
|
|
2397
|
+
await new Promise((resolve) => {
|
|
2398
|
+
const doShutdown = async () => {
|
|
2399
|
+
await shutdown(state, bucketDeps, context);
|
|
2400
|
+
resolve();
|
|
2401
|
+
};
|
|
2402
|
+
const onKey = createKeypressHandler(useTUI, state, () => {
|
|
2403
|
+
doShutdown();
|
|
2404
|
+
});
|
|
2405
|
+
cleanupKeypress = bucketDeps.setupKeypress(onKey);
|
|
2406
|
+
cleanupSignals = bucketDeps.setupSignals(() => {
|
|
2407
|
+
doShutdown();
|
|
2408
|
+
});
|
|
2409
|
+
connectWebSocket(token, state, bucketDeps, context, fileSystem, useTUI, initialWs);
|
|
2410
|
+
if (!useTUI) {
|
|
2411
|
+
context.stdout(" Press q or Ctrl+C to exit");
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
} finally {
|
|
2415
|
+
tuiHandle?.cleanup();
|
|
2416
|
+
cleanupKeypress?.();
|
|
2417
|
+
cleanupSignals?.();
|
|
2418
|
+
}
|
|
2419
|
+
context.stdout("Goodbye!");
|
|
1360
2420
|
return { exitCode: 0 };
|
|
1361
2421
|
}
|
|
1362
2422
|
|
|
1363
|
-
// lib/cli/
|
|
1364
|
-
import {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
2423
|
+
// lib/cli/process-runner.ts
|
|
2424
|
+
import { spawn } from "node:child_process";
|
|
2425
|
+
function createShellRunner(spawnFn) {
|
|
2426
|
+
return {
|
|
2427
|
+
run: (command, cwd, timeoutMs) => runBufferedProcess(spawnFn, command, [], cwd, true, timeoutMs)
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
var defaultShellRunner = createShellRunner(spawn);
|
|
2431
|
+
function createGitRunner(spawnFn) {
|
|
2432
|
+
return {
|
|
2433
|
+
run: (gitArguments, cwd) => runBufferedProcess(spawnFn, "git", gitArguments, cwd, false)
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
var defaultGitRunner = createGitRunner(spawn);
|
|
2437
|
+
function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, timeoutMs) {
|
|
2438
|
+
return new Promise((resolve) => {
|
|
2439
|
+
const proc = spawnFn(command, commandArguments, { cwd, shell });
|
|
2440
|
+
const chunks = [];
|
|
2441
|
+
let resolved = false;
|
|
2442
|
+
let timer;
|
|
2443
|
+
if (timeoutMs !== undefined) {
|
|
2444
|
+
timer = setTimeout(() => {
|
|
2445
|
+
resolved = true;
|
|
2446
|
+
proc.kill();
|
|
2447
|
+
resolve({
|
|
2448
|
+
exitCode: 1,
|
|
2449
|
+
output: chunks.join(""),
|
|
2450
|
+
timedOut: true
|
|
2451
|
+
});
|
|
2452
|
+
}, timeoutMs);
|
|
2453
|
+
}
|
|
2454
|
+
proc.stdout?.on("data", (data) => {
|
|
2455
|
+
chunks.push(data.toString());
|
|
2456
|
+
});
|
|
2457
|
+
proc.stderr?.on("data", (data) => {
|
|
2458
|
+
chunks.push(data.toString());
|
|
2459
|
+
});
|
|
2460
|
+
proc.on("close", (code) => {
|
|
2461
|
+
if (resolved)
|
|
2462
|
+
return;
|
|
2463
|
+
if (timer !== undefined)
|
|
2464
|
+
clearTimeout(timer);
|
|
2465
|
+
resolve({ exitCode: code ?? 1, output: chunks.join("") });
|
|
2466
|
+
});
|
|
2467
|
+
proc.on("error", (error) => {
|
|
2468
|
+
if (resolved)
|
|
2469
|
+
return;
|
|
2470
|
+
if (timer !== undefined)
|
|
2471
|
+
clearTimeout(timer);
|
|
2472
|
+
resolve({ exitCode: 1, output: error.message });
|
|
2473
|
+
});
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// lib/cli/commands/lint-markdown.ts
|
|
2478
|
+
import { dirname as dirname3, resolve } from "node:path";
|
|
2479
|
+
|
|
2480
|
+
// lib/workflow-tasks.ts
|
|
2481
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
2482
|
+
"Refine Idea: ",
|
|
2483
|
+
"Decompose Idea: ",
|
|
2484
|
+
"Shelve Idea: "
|
|
2485
|
+
];
|
|
2486
|
+
function titleToFilename(title) {
|
|
2487
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// lib/cli/commands/lint-markdown.ts
|
|
2491
|
+
var REQUIRED_HEADINGS = ["## Goals", "## Blocked By", "## Definition of Done"];
|
|
2492
|
+
var REQUIRED_GOAL_HEADINGS = ["## Parent Goal", "## Sub-Goals"];
|
|
2493
|
+
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
2494
|
+
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
2495
|
+
function validateFilename(filePath) {
|
|
2496
|
+
const parts = filePath.split("/");
|
|
2497
|
+
const filename = parts[parts.length - 1];
|
|
2498
|
+
if (!SLUG_PATTERN.test(filename)) {
|
|
2499
|
+
return {
|
|
2500
|
+
file: filePath,
|
|
2501
|
+
message: `Filename "${filename}" does not match slug-style naming`
|
|
2502
|
+
};
|
|
1389
2503
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
function validateTitleFilenameMatch(filePath, content) {
|
|
2507
|
+
const title = extractTitle(content);
|
|
2508
|
+
if (!title) {
|
|
2509
|
+
return null;
|
|
2510
|
+
}
|
|
2511
|
+
const parts = filePath.split("/");
|
|
2512
|
+
const actualFilename = parts[parts.length - 1];
|
|
2513
|
+
const expectedFilename = titleToFilename(title);
|
|
2514
|
+
if (actualFilename !== expectedFilename) {
|
|
2515
|
+
return {
|
|
2516
|
+
file: filePath,
|
|
2517
|
+
message: `Filename "${actualFilename}" does not match title "${title}" (expected "${expectedFilename}")`
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
function validateOpeningSentence(filePath, content) {
|
|
2523
|
+
const openingSentence = extractOpeningSentence(content);
|
|
2524
|
+
if (!openingSentence) {
|
|
2525
|
+
return {
|
|
2526
|
+
file: filePath,
|
|
2527
|
+
message: "Missing or malformed opening sentence after H1 heading"
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
function validateOpeningSentenceLength(filePath, content) {
|
|
2533
|
+
const openingSentence = extractOpeningSentence(content);
|
|
2534
|
+
if (!openingSentence) {
|
|
2535
|
+
return null;
|
|
2536
|
+
}
|
|
2537
|
+
if (openingSentence.length > MAX_OPENING_SENTENCE_LENGTH) {
|
|
2538
|
+
return {
|
|
2539
|
+
file: filePath,
|
|
2540
|
+
message: `Opening sentence is ${openingSentence.length} characters (max ${MAX_OPENING_SENTENCE_LENGTH}). Split into multiple sentences; only the first sentence is checked.`
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
var NON_IMPERATIVE_STARTERS = new Set([
|
|
2546
|
+
"the",
|
|
2547
|
+
"a",
|
|
2548
|
+
"an",
|
|
2549
|
+
"this",
|
|
2550
|
+
"that",
|
|
2551
|
+
"these",
|
|
2552
|
+
"those",
|
|
2553
|
+
"we",
|
|
2554
|
+
"it",
|
|
2555
|
+
"they",
|
|
2556
|
+
"you",
|
|
2557
|
+
"i"
|
|
2558
|
+
]);
|
|
2559
|
+
function validateImperativeOpeningSentence(filePath, content) {
|
|
2560
|
+
const openingSentence = extractOpeningSentence(content);
|
|
2561
|
+
if (!openingSentence) {
|
|
2562
|
+
return null;
|
|
1393
2563
|
}
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const
|
|
1398
|
-
if (rel) {
|
|
1399
|
-
for (const childPath of rel.subGoals) {
|
|
1400
|
-
children.push(buildNode(childPath));
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
2564
|
+
const firstWord = openingSentence.split(/\s/)[0].replace(/[^a-zA-Z]/g, "");
|
|
2565
|
+
const lower = firstWord.toLowerCase();
|
|
2566
|
+
if (NON_IMPERATIVE_STARTERS.has(lower) || lower.endsWith("ing")) {
|
|
2567
|
+
const preview = openingSentence.length > 40 ? `${openingSentence.slice(0, 40)}...` : openingSentence;
|
|
1403
2568
|
return {
|
|
1404
|
-
filePath,
|
|
1405
|
-
|
|
1406
|
-
children
|
|
2569
|
+
file: filePath,
|
|
2570
|
+
message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
|
|
1407
2571
|
};
|
|
1408
2572
|
}
|
|
1409
|
-
return
|
|
2573
|
+
return null;
|
|
1410
2574
|
}
|
|
1411
|
-
function
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
renderHierarchy(node.children, output, prefix + childPrefix);
|
|
2575
|
+
function validateTaskHeadings(filePath, content) {
|
|
2576
|
+
const violations = [];
|
|
2577
|
+
for (const heading of REQUIRED_HEADINGS) {
|
|
2578
|
+
if (!content.includes(heading)) {
|
|
2579
|
+
violations.push({
|
|
2580
|
+
file: filePath,
|
|
2581
|
+
message: `Missing required heading: "${heading}"`
|
|
2582
|
+
});
|
|
1420
2583
|
}
|
|
1421
2584
|
}
|
|
2585
|
+
return violations;
|
|
1422
2586
|
}
|
|
1423
|
-
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (mdFiles.length === 0) {
|
|
1445
|
-
if (specificTypeRequested) {
|
|
1446
|
-
context.stdout(SECTION_HEADERS[type]);
|
|
1447
|
-
context.stdout("");
|
|
1448
|
-
context.stdout(TYPE_EXPLANATIONS[type]);
|
|
1449
|
-
context.stdout("");
|
|
1450
|
-
context.stdout(`No ${type} found.`);
|
|
1451
|
-
context.stdout("");
|
|
2587
|
+
function validateLinks(filePath, content, fileSystem) {
|
|
2588
|
+
const violations = [];
|
|
2589
|
+
const lines = content.split(`
|
|
2590
|
+
`);
|
|
2591
|
+
const fileDir = dirname3(filePath);
|
|
2592
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2593
|
+
const line = lines[i];
|
|
2594
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
2595
|
+
let match = linkPattern.exec(line);
|
|
2596
|
+
while (match) {
|
|
2597
|
+
const linkTarget = match[2];
|
|
2598
|
+
if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
|
|
2599
|
+
const targetPath = linkTarget.split("#")[0];
|
|
2600
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
2601
|
+
if (!fileSystem.exists(resolvedPath)) {
|
|
2602
|
+
violations.push({
|
|
2603
|
+
file: filePath,
|
|
2604
|
+
message: `Broken link: "${linkTarget}"`,
|
|
2605
|
+
line: i + 1
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
1452
2608
|
}
|
|
1453
|
-
|
|
2609
|
+
match = linkPattern.exec(line);
|
|
1454
2610
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
2611
|
+
}
|
|
2612
|
+
return violations;
|
|
2613
|
+
}
|
|
2614
|
+
function validateIdeaOpenQuestions(filePath, content) {
|
|
2615
|
+
const violations = [];
|
|
2616
|
+
const lines = content.split(`
|
|
2617
|
+
`);
|
|
2618
|
+
let inOpenQuestions = false;
|
|
2619
|
+
let currentQuestionLine = null;
|
|
2620
|
+
let inCodeBlock = false;
|
|
2621
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2622
|
+
const line = lines[i];
|
|
2623
|
+
if (line.startsWith("```")) {
|
|
2624
|
+
inCodeBlock = !inCodeBlock;
|
|
2625
|
+
continue;
|
|
1466
2626
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
|
|
2627
|
+
if (inCodeBlock)
|
|
2628
|
+
continue;
|
|
2629
|
+
if (line.startsWith("## ")) {
|
|
2630
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
2631
|
+
violations.push({
|
|
2632
|
+
file: filePath,
|
|
2633
|
+
message: "Question has no options listed beneath it",
|
|
2634
|
+
line: currentQuestionLine
|
|
2635
|
+
});
|
|
1477
2636
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
2637
|
+
const headingText = line.slice(3).trimEnd();
|
|
2638
|
+
if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
|
|
2639
|
+
violations.push({
|
|
2640
|
+
file: filePath,
|
|
2641
|
+
message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
|
|
2642
|
+
line: i + 1
|
|
2643
|
+
});
|
|
1480
2644
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
2645
|
+
inOpenQuestions = line === "## Open Questions";
|
|
2646
|
+
currentQuestionLine = null;
|
|
2647
|
+
continue;
|
|
1483
2648
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
"--output-format",
|
|
1513
|
-
"stream-json",
|
|
1514
|
-
"--verbose",
|
|
1515
|
-
"--include-partial-messages"
|
|
1516
|
-
];
|
|
1517
|
-
if (allowedTools?.length) {
|
|
1518
|
-
claudeArguments.push("--allowedTools", ...allowedTools);
|
|
1519
|
-
}
|
|
1520
|
-
if (maxTurns) {
|
|
1521
|
-
claudeArguments.push("--max-turns", String(maxTurns));
|
|
1522
|
-
}
|
|
1523
|
-
if (model) {
|
|
1524
|
-
claudeArguments.push("--model", model);
|
|
1525
|
-
}
|
|
1526
|
-
if (systemPrompt) {
|
|
1527
|
-
claudeArguments.push("--system-prompt", systemPrompt);
|
|
1528
|
-
}
|
|
1529
|
-
if (sessionId) {
|
|
1530
|
-
claudeArguments.push("--session-id", sessionId);
|
|
1531
|
-
}
|
|
1532
|
-
if (dangerouslySkipPermissions) {
|
|
1533
|
-
claudeArguments.push("--dangerously-skip-permissions");
|
|
1534
|
-
}
|
|
1535
|
-
const proc = dependencies.spawn("claude", claudeArguments, {
|
|
1536
|
-
cwd,
|
|
1537
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1538
|
-
env: { ...process.env, ...env }
|
|
1539
|
-
});
|
|
1540
|
-
if (!proc.stdout) {
|
|
1541
|
-
throw new Error("Failed to get stdout from claude process");
|
|
1542
|
-
}
|
|
1543
|
-
const rl = dependencies.createInterface({ input: proc.stdout });
|
|
1544
|
-
for await (const line of rl) {
|
|
1545
|
-
if (!line.trim())
|
|
2649
|
+
if (!inOpenQuestions)
|
|
2650
|
+
continue;
|
|
2651
|
+
if (/^[-*] /.test(line.trimStart())) {
|
|
2652
|
+
violations.push({
|
|
2653
|
+
file: filePath,
|
|
2654
|
+
message: "Open Questions must use ### headings for questions and #### headings for options, not bullet points. Run `dust new idea` to see the expected format.",
|
|
2655
|
+
line: i + 1
|
|
2656
|
+
});
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (line.startsWith("### ")) {
|
|
2660
|
+
if (currentQuestionLine !== null) {
|
|
2661
|
+
violations.push({
|
|
2662
|
+
file: filePath,
|
|
2663
|
+
message: "Question has no options listed beneath it",
|
|
2664
|
+
line: currentQuestionLine
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
if (!line.trimEnd().endsWith("?")) {
|
|
2668
|
+
violations.push({
|
|
2669
|
+
file: filePath,
|
|
2670
|
+
message: 'Questions must end with "?" (e.g., "### Should we take our own payments?")',
|
|
2671
|
+
line: i + 1
|
|
2672
|
+
});
|
|
2673
|
+
currentQuestionLine = null;
|
|
2674
|
+
} else {
|
|
2675
|
+
currentQuestionLine = i + 1;
|
|
2676
|
+
}
|
|
1546
2677
|
continue;
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
2678
|
+
}
|
|
2679
|
+
if (line.startsWith("#### ")) {
|
|
2680
|
+
currentQuestionLine = null;
|
|
2681
|
+
}
|
|
1550
2682
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
reject(new Error(`claude exited with code ${code}`));
|
|
2683
|
+
if (inOpenQuestions && currentQuestionLine !== null) {
|
|
2684
|
+
violations.push({
|
|
2685
|
+
file: filePath,
|
|
2686
|
+
message: "Question has no options listed beneath it",
|
|
2687
|
+
line: currentQuestionLine
|
|
1557
2688
|
});
|
|
1558
|
-
|
|
1559
|
-
|
|
2689
|
+
}
|
|
2690
|
+
return violations;
|
|
1560
2691
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2692
|
+
var SEMANTIC_RULES = [
|
|
2693
|
+
{
|
|
2694
|
+
section: "## Goals",
|
|
2695
|
+
requiredPath: "/.dust/goals/",
|
|
2696
|
+
description: "goal"
|
|
2697
|
+
},
|
|
2698
|
+
{
|
|
2699
|
+
section: "## Blocked By",
|
|
2700
|
+
requiredPath: "/.dust/tasks/",
|
|
2701
|
+
description: "task"
|
|
2702
|
+
}
|
|
2703
|
+
];
|
|
2704
|
+
function validateSemanticLinks(filePath, content) {
|
|
2705
|
+
const violations = [];
|
|
2706
|
+
const lines = content.split(`
|
|
2707
|
+
`);
|
|
2708
|
+
const fileDir = dirname3(filePath);
|
|
2709
|
+
let currentSection = null;
|
|
2710
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2711
|
+
const line = lines[i];
|
|
2712
|
+
if (line.startsWith("## ")) {
|
|
2713
|
+
currentSection = line;
|
|
2714
|
+
continue;
|
|
1568
2715
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
2716
|
+
const rule = SEMANTIC_RULES.find((r) => r.section === currentSection);
|
|
2717
|
+
if (!rule)
|
|
2718
|
+
continue;
|
|
2719
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
2720
|
+
let match = linkPattern.exec(line);
|
|
2721
|
+
while (match) {
|
|
2722
|
+
const linkTarget = match[2];
|
|
2723
|
+
if (linkTarget.startsWith("#")) {
|
|
2724
|
+
violations.push({
|
|
2725
|
+
file: filePath,
|
|
2726
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an anchor: "${linkTarget}"`,
|
|
2727
|
+
line: i + 1
|
|
2728
|
+
});
|
|
2729
|
+
match = linkPattern.exec(line);
|
|
2730
|
+
continue;
|
|
2731
|
+
}
|
|
2732
|
+
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
2733
|
+
violations.push({
|
|
2734
|
+
file: filePath,
|
|
2735
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an external URL: "${linkTarget}"`,
|
|
2736
|
+
line: i + 1
|
|
2737
|
+
});
|
|
2738
|
+
match = linkPattern.exec(line);
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const targetPath = linkTarget.split("#")[0];
|
|
2742
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
2743
|
+
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
2744
|
+
violations.push({
|
|
2745
|
+
file: filePath,
|
|
2746
|
+
message: `Link in "${rule.section}" must point to a ${rule.description} file: "${linkTarget}"`,
|
|
2747
|
+
line: i + 1
|
|
2748
|
+
});
|
|
1581
2749
|
}
|
|
2750
|
+
match = linkPattern.exec(line);
|
|
1582
2751
|
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2752
|
+
}
|
|
2753
|
+
return violations;
|
|
2754
|
+
}
|
|
2755
|
+
function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
2756
|
+
const title = extractTitle(content);
|
|
2757
|
+
if (!title) {
|
|
2758
|
+
return null;
|
|
2759
|
+
}
|
|
2760
|
+
for (const prefix of IDEA_TRANSITION_PREFIXES) {
|
|
2761
|
+
if (title.startsWith(prefix)) {
|
|
2762
|
+
const ideaTitle = title.slice(prefix.length);
|
|
2763
|
+
const ideaFilename = titleToFilename(ideaTitle);
|
|
2764
|
+
if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
|
|
2765
|
+
return {
|
|
2766
|
+
file: filePath,
|
|
2767
|
+
message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
|
|
1591
2768
|
};
|
|
1592
2769
|
}
|
|
2770
|
+
return null;
|
|
1593
2771
|
}
|
|
1594
|
-
} else if (raw.type === "result") {
|
|
1595
|
-
const r = raw;
|
|
1596
|
-
yield {
|
|
1597
|
-
type: "result",
|
|
1598
|
-
subtype: r.subtype ?? "success",
|
|
1599
|
-
result: r.result,
|
|
1600
|
-
error: r.error,
|
|
1601
|
-
cost_usd: r.total_cost_usd ?? r.cost_usd ?? 0,
|
|
1602
|
-
duration_ms: r.duration_ms ?? 0,
|
|
1603
|
-
num_turns: r.num_turns ?? 0,
|
|
1604
|
-
session_id: r.session_id ?? ""
|
|
1605
|
-
};
|
|
1606
2772
|
}
|
|
2773
|
+
return null;
|
|
1607
2774
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
lines.push(`\uD83D\uDD27 Write: ${filePath ?? "(unknown)"}`);
|
|
1617
|
-
lines.push(DIVIDER);
|
|
1618
|
-
if (content !== undefined) {
|
|
1619
|
-
for (const line of content.split(`
|
|
1620
|
-
`)) {
|
|
1621
|
-
lines.push(line);
|
|
2775
|
+
function validateGoalHierarchySections(filePath, content) {
|
|
2776
|
+
const violations = [];
|
|
2777
|
+
for (const heading of REQUIRED_GOAL_HEADINGS) {
|
|
2778
|
+
if (!content.includes(heading)) {
|
|
2779
|
+
violations.push({
|
|
2780
|
+
file: filePath,
|
|
2781
|
+
message: `Missing required heading: "${heading}"`
|
|
2782
|
+
});
|
|
1622
2783
|
}
|
|
1623
2784
|
}
|
|
1624
|
-
|
|
1625
|
-
appendOtherArgs(lines, others);
|
|
1626
|
-
return lines;
|
|
2785
|
+
return violations;
|
|
1627
2786
|
}
|
|
1628
|
-
function
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
"
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2787
|
+
function validateGoalHierarchyLinks(filePath, content) {
|
|
2788
|
+
const violations = [];
|
|
2789
|
+
const lines = content.split(`
|
|
2790
|
+
`);
|
|
2791
|
+
const fileDir = dirname3(filePath);
|
|
2792
|
+
let currentSection = null;
|
|
2793
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2794
|
+
const line = lines[i];
|
|
2795
|
+
if (line.startsWith("## ")) {
|
|
2796
|
+
currentSection = line;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
2803
|
+
let match = linkPattern.exec(line);
|
|
2804
|
+
while (match) {
|
|
2805
|
+
const linkTarget = match[2];
|
|
2806
|
+
if (linkTarget.startsWith("#")) {
|
|
2807
|
+
violations.push({
|
|
2808
|
+
file: filePath,
|
|
2809
|
+
message: `Link in "${currentSection}" must point to a goal file, not an anchor: "${linkTarget}"`,
|
|
2810
|
+
line: i + 1
|
|
2811
|
+
});
|
|
2812
|
+
match = linkPattern.exec(line);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
2816
|
+
violations.push({
|
|
2817
|
+
file: filePath,
|
|
2818
|
+
message: `Link in "${currentSection}" must point to a goal file, not an external URL: "${linkTarget}"`,
|
|
2819
|
+
line: i + 1
|
|
2820
|
+
});
|
|
2821
|
+
match = linkPattern.exec(line);
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
const targetPath = linkTarget.split("#")[0];
|
|
2825
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
2826
|
+
if (!resolvedPath.includes("/.dust/goals/")) {
|
|
2827
|
+
violations.push({
|
|
2828
|
+
file: filePath,
|
|
2829
|
+
message: `Link in "${currentSection}" must point to a goal file: "${linkTarget}"`,
|
|
2830
|
+
line: i + 1
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
match = linkPattern.exec(line);
|
|
1646
2834
|
}
|
|
1647
2835
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
2836
|
+
return violations;
|
|
2837
|
+
}
|
|
2838
|
+
function extractGoalRelationships(filePath, content) {
|
|
2839
|
+
const lines = content.split(`
|
|
2840
|
+
`);
|
|
2841
|
+
const fileDir = dirname3(filePath);
|
|
2842
|
+
const parentGoals = [];
|
|
2843
|
+
const subGoals = [];
|
|
2844
|
+
let currentSection = null;
|
|
2845
|
+
for (const line of lines) {
|
|
2846
|
+
if (line.startsWith("## ")) {
|
|
2847
|
+
currentSection = line;
|
|
2848
|
+
continue;
|
|
2849
|
+
}
|
|
2850
|
+
if (currentSection !== "## Parent Goal" && currentSection !== "## Sub-Goals") {
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
2854
|
+
let match = linkPattern.exec(line);
|
|
2855
|
+
while (match) {
|
|
2856
|
+
const linkTarget = match[2];
|
|
2857
|
+
if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
|
|
2858
|
+
const targetPath = linkTarget.split("#")[0];
|
|
2859
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
2860
|
+
if (resolvedPath.includes("/.dust/goals/")) {
|
|
2861
|
+
if (currentSection === "## Parent Goal") {
|
|
2862
|
+
parentGoals.push(resolvedPath);
|
|
2863
|
+
} else {
|
|
2864
|
+
subGoals.push(resolvedPath);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
match = linkPattern.exec(line);
|
|
1655
2869
|
}
|
|
1656
2870
|
}
|
|
1657
|
-
|
|
1658
|
-
appendOtherArgs(lines, others);
|
|
1659
|
-
return lines;
|
|
2871
|
+
return { filePath, parentGoals, subGoals };
|
|
1660
2872
|
}
|
|
1661
|
-
function
|
|
1662
|
-
const
|
|
1663
|
-
const
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
const lines = [];
|
|
1667
|
-
let lineRange = "";
|
|
1668
|
-
if (offset !== undefined || limit !== undefined) {
|
|
1669
|
-
const start = offset ?? 1;
|
|
1670
|
-
const end = limit !== undefined ? start + limit - 1 : undefined;
|
|
1671
|
-
lineRange = end !== undefined ? ` (lines ${start}-${end})` : ` (from line ${start})`;
|
|
2873
|
+
function validateBidirectionalLinks(allGoalRelationships) {
|
|
2874
|
+
const violations = [];
|
|
2875
|
+
const relationshipMap = new Map;
|
|
2876
|
+
for (const rel of allGoalRelationships) {
|
|
2877
|
+
relationshipMap.set(rel.filePath, rel);
|
|
1672
2878
|
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
lines.push(`$ ${command}`);
|
|
2879
|
+
for (const rel of allGoalRelationships) {
|
|
2880
|
+
for (const parentPath of rel.parentGoals) {
|
|
2881
|
+
const parentRel = relationshipMap.get(parentPath);
|
|
2882
|
+
if (parentRel && !parentRel.subGoals.includes(rel.filePath)) {
|
|
2883
|
+
violations.push({
|
|
2884
|
+
file: rel.filePath,
|
|
2885
|
+
message: `Parent goal "${parentPath}" does not list this goal as a sub-goal`
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
for (const subGoalPath of rel.subGoals) {
|
|
2890
|
+
const subGoalRel = relationshipMap.get(subGoalPath);
|
|
2891
|
+
if (subGoalRel && !subGoalRel.parentGoals.includes(rel.filePath)) {
|
|
2892
|
+
violations.push({
|
|
2893
|
+
file: rel.filePath,
|
|
2894
|
+
message: `Sub-goal "${subGoalPath}" does not list this goal as its parent`
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
1693
2898
|
}
|
|
1694
|
-
|
|
1695
|
-
return lines;
|
|
2899
|
+
return violations;
|
|
1696
2900
|
}
|
|
1697
|
-
function
|
|
1698
|
-
const
|
|
1699
|
-
const
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
2901
|
+
function validateNoCycles(allGoalRelationships) {
|
|
2902
|
+
const violations = [];
|
|
2903
|
+
const relationshipMap = new Map;
|
|
2904
|
+
for (const rel of allGoalRelationships) {
|
|
2905
|
+
relationshipMap.set(rel.filePath, rel);
|
|
2906
|
+
}
|
|
2907
|
+
for (const rel of allGoalRelationships) {
|
|
2908
|
+
const visited = new Set;
|
|
2909
|
+
const path = [];
|
|
2910
|
+
let current = rel.filePath;
|
|
2911
|
+
while (current) {
|
|
2912
|
+
if (visited.has(current)) {
|
|
2913
|
+
const cycleStart = path.indexOf(current);
|
|
2914
|
+
const cyclePath = path.slice(cycleStart).concat(current);
|
|
2915
|
+
violations.push({
|
|
2916
|
+
file: rel.filePath,
|
|
2917
|
+
message: `Cycle detected in goal hierarchy: ${cyclePath.join(" -> ")}`
|
|
2918
|
+
});
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
visited.add(current);
|
|
2922
|
+
path.push(current);
|
|
2923
|
+
const currentRel = relationshipMap.get(current);
|
|
2924
|
+
if (currentRel && currentRel.parentGoals.length > 0) {
|
|
2925
|
+
current = currentRel.parentGoals[0];
|
|
2926
|
+
} else {
|
|
2927
|
+
current = null;
|
|
2928
|
+
}
|
|
1707
2929
|
}
|
|
1708
2930
|
}
|
|
1709
|
-
|
|
1710
|
-
return lines;
|
|
2931
|
+
return violations;
|
|
1711
2932
|
}
|
|
1712
|
-
function
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
"
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
"-A",
|
|
1725
|
-
"-B",
|
|
1726
|
-
"-C",
|
|
1727
|
-
"-i",
|
|
1728
|
-
"-n",
|
|
1729
|
-
"head_limit",
|
|
1730
|
-
"offset",
|
|
1731
|
-
"multiline"
|
|
1732
|
-
]);
|
|
1733
|
-
const lines = [];
|
|
1734
|
-
const location = path ?? ".";
|
|
1735
|
-
let filter = "";
|
|
1736
|
-
if (glob) {
|
|
1737
|
-
filter = ` (${glob})`;
|
|
1738
|
-
} else if (type) {
|
|
1739
|
-
filter = ` (type: ${type})`;
|
|
2933
|
+
async function safeScanDir(glob, dirPath) {
|
|
2934
|
+
const files = [];
|
|
2935
|
+
try {
|
|
2936
|
+
for await (const file of glob.scan(dirPath)) {
|
|
2937
|
+
files.push(file);
|
|
2938
|
+
}
|
|
2939
|
+
return { files, exists: true };
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
if (error.code === "ENOENT") {
|
|
2942
|
+
return { files: [], exists: false };
|
|
2943
|
+
}
|
|
2944
|
+
throw error;
|
|
1740
2945
|
}
|
|
1741
|
-
lines.push(`\uD83D\uDD27 Grep: "${pattern ?? ""}" in ${location}${filter}`);
|
|
1742
|
-
appendOtherArgs(lines, others);
|
|
1743
|
-
return lines;
|
|
1744
|
-
}
|
|
1745
|
-
function formatGlob(input) {
|
|
1746
|
-
const pattern = input.pattern;
|
|
1747
|
-
const path = input.path;
|
|
1748
|
-
const others = getUnrecognizedArgs(input, ["pattern", "path"]);
|
|
1749
|
-
const lines = [];
|
|
1750
|
-
const location = path ?? ".";
|
|
1751
|
-
lines.push(`\uD83D\uDD27 Glob: ${pattern ?? ""} in ${location}`);
|
|
1752
|
-
appendOtherArgs(lines, others);
|
|
1753
|
-
return lines;
|
|
1754
2946
|
}
|
|
1755
|
-
function
|
|
1756
|
-
const
|
|
1757
|
-
const
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
"
|
|
1761
|
-
"
|
|
1762
|
-
|
|
1763
|
-
"model",
|
|
1764
|
-
"max_turns",
|
|
1765
|
-
"resume",
|
|
1766
|
-
"run_in_background"
|
|
1767
|
-
]);
|
|
1768
|
-
const lines = [];
|
|
1769
|
-
const header = description ?? subagentType ?? "task";
|
|
1770
|
-
lines.push(`\uD83D\uDD27 Task: ${header}`);
|
|
1771
|
-
if (prompt !== undefined) {
|
|
1772
|
-
const truncated = prompt.length > 100 ? `${prompt.slice(0, 100)}...` : prompt;
|
|
1773
|
-
lines.push(`"${truncated}"`);
|
|
2947
|
+
async function lintMarkdown(dependencies) {
|
|
2948
|
+
const { context, fileSystem, globScanner: glob } = dependencies;
|
|
2949
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
2950
|
+
const dustScan = await safeScanDir(glob, dustPath);
|
|
2951
|
+
if (!dustScan.exists) {
|
|
2952
|
+
context.stderr("Error: .dust directory not found");
|
|
2953
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
2954
|
+
return { exitCode: 1 };
|
|
1774
2955
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
Grep: formatGrep,
|
|
1791
|
-
Glob: formatGlob,
|
|
1792
|
-
Task: formatTask
|
|
1793
|
-
};
|
|
1794
|
-
function formatToolUse(name, input) {
|
|
1795
|
-
const formatter = formatters[name];
|
|
1796
|
-
if (formatter) {
|
|
1797
|
-
return formatter(input);
|
|
2956
|
+
const dustFiles = dustScan.files;
|
|
2957
|
+
const violations = [];
|
|
2958
|
+
context.stdout("Validating links in .dust/...");
|
|
2959
|
+
for (const file of dustFiles) {
|
|
2960
|
+
if (!file.endsWith(".md"))
|
|
2961
|
+
continue;
|
|
2962
|
+
const filePath = `${dustPath}/${file}`;
|
|
2963
|
+
try {
|
|
2964
|
+
const content = await fileSystem.readFile(filePath);
|
|
2965
|
+
violations.push(...validateLinks(filePath, content, fileSystem));
|
|
2966
|
+
} catch (error) {
|
|
2967
|
+
if (error.code !== "ENOENT") {
|
|
2968
|
+
throw error;
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
1798
2971
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2972
|
+
const contentDirs = ["goals", "facts", "ideas", "tasks"];
|
|
2973
|
+
context.stdout("Validating content files...");
|
|
2974
|
+
for (const dir of contentDirs) {
|
|
2975
|
+
const dirPath = `${dustPath}/${dir}`;
|
|
2976
|
+
const { files } = await safeScanDir(glob, dirPath);
|
|
2977
|
+
for (const file of files) {
|
|
2978
|
+
if (!file.endsWith(".md"))
|
|
2979
|
+
continue;
|
|
2980
|
+
const filePath = `${dirPath}/${file}`;
|
|
2981
|
+
let content;
|
|
2982
|
+
try {
|
|
2983
|
+
content = await fileSystem.readFile(filePath);
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
if (error.code === "ENOENT") {
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
throw error;
|
|
2989
|
+
}
|
|
2990
|
+
const openingSentenceViolation = validateOpeningSentence(filePath, content);
|
|
2991
|
+
if (openingSentenceViolation) {
|
|
2992
|
+
violations.push(openingSentenceViolation);
|
|
2993
|
+
}
|
|
2994
|
+
const openingSentenceLengthViolation = validateOpeningSentenceLength(filePath, content);
|
|
2995
|
+
if (openingSentenceLengthViolation) {
|
|
2996
|
+
violations.push(openingSentenceLengthViolation);
|
|
2997
|
+
}
|
|
2998
|
+
const titleFilenameViolation = validateTitleFilenameMatch(filePath, content);
|
|
2999
|
+
if (titleFilenameViolation) {
|
|
3000
|
+
violations.push(titleFilenameViolation);
|
|
3001
|
+
}
|
|
1806
3002
|
}
|
|
1807
3003
|
}
|
|
1808
|
-
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
processEvent(event, sink, { hadTextOutput });
|
|
1825
|
-
if (event.type === "text_delta") {
|
|
1826
|
-
hadTextOutput = true;
|
|
1827
|
-
} else if (event.type === "tool_use") {
|
|
1828
|
-
hadTextOutput = false;
|
|
3004
|
+
const ideasPath = `${dustPath}/ideas`;
|
|
3005
|
+
const { files: ideaFiles } = await safeScanDir(glob, ideasPath);
|
|
3006
|
+
if (ideaFiles.length > 0) {
|
|
3007
|
+
context.stdout("Validating idea files in .dust/ideas/...");
|
|
3008
|
+
for (const file of ideaFiles) {
|
|
3009
|
+
if (!file.endsWith(".md"))
|
|
3010
|
+
continue;
|
|
3011
|
+
const filePath = `${ideasPath}/${file}`;
|
|
3012
|
+
let content;
|
|
3013
|
+
try {
|
|
3014
|
+
content = await fileSystem.readFile(filePath);
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
if (error.code === "ENOENT") {
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
throw error;
|
|
1829
3020
|
}
|
|
3021
|
+
violations.push(...validateIdeaOpenQuestions(filePath, content));
|
|
1830
3022
|
}
|
|
1831
3023
|
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
3024
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
3025
|
+
const { files: taskFiles } = await safeScanDir(glob, tasksPath);
|
|
3026
|
+
if (taskFiles.length > 0) {
|
|
3027
|
+
context.stdout("Validating task files in .dust/tasks/...");
|
|
3028
|
+
for (const file of taskFiles) {
|
|
3029
|
+
if (!file.endsWith(".md"))
|
|
3030
|
+
continue;
|
|
3031
|
+
const filePath = `${tasksPath}/${file}`;
|
|
3032
|
+
let content;
|
|
3033
|
+
try {
|
|
3034
|
+
content = await fileSystem.readFile(filePath);
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
if (error.code === "ENOENT") {
|
|
3037
|
+
continue;
|
|
3038
|
+
}
|
|
3039
|
+
throw error;
|
|
1842
3040
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
3041
|
+
const filenameViolation = validateFilename(filePath);
|
|
3042
|
+
if (filenameViolation) {
|
|
3043
|
+
violations.push(filenameViolation);
|
|
1845
3044
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
3045
|
+
violations.push(...validateTaskHeadings(filePath, content));
|
|
3046
|
+
violations.push(...validateSemanticLinks(filePath, content));
|
|
3047
|
+
const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
|
|
3048
|
+
if (imperativeViolation) {
|
|
3049
|
+
violations.push(imperativeViolation);
|
|
3050
|
+
}
|
|
3051
|
+
const ideaTransitionViolation = validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem);
|
|
3052
|
+
if (ideaTransitionViolation) {
|
|
3053
|
+
violations.push(ideaTransitionViolation);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
const goalsPath = `${dustPath}/goals`;
|
|
3058
|
+
const { files: goalFiles } = await safeScanDir(glob, goalsPath);
|
|
3059
|
+
if (goalFiles.length > 0) {
|
|
3060
|
+
context.stdout("Validating goal hierarchy in .dust/goals/...");
|
|
3061
|
+
const allGoalRelationships = [];
|
|
3062
|
+
for (const file of goalFiles) {
|
|
3063
|
+
if (!file.endsWith(".md"))
|
|
3064
|
+
continue;
|
|
3065
|
+
const filePath = `${goalsPath}/${file}`;
|
|
3066
|
+
let content;
|
|
3067
|
+
try {
|
|
3068
|
+
content = await fileSystem.readFile(filePath);
|
|
3069
|
+
} catch (error) {
|
|
3070
|
+
if (error.code === "ENOENT") {
|
|
3071
|
+
continue;
|
|
3072
|
+
}
|
|
3073
|
+
throw error;
|
|
3074
|
+
}
|
|
3075
|
+
violations.push(...validateGoalHierarchySections(filePath, content));
|
|
3076
|
+
violations.push(...validateGoalHierarchyLinks(filePath, content));
|
|
3077
|
+
allGoalRelationships.push(extractGoalRelationships(filePath, content));
|
|
3078
|
+
}
|
|
3079
|
+
violations.push(...validateBidirectionalLinks(allGoalRelationships));
|
|
3080
|
+
violations.push(...validateNoCycles(allGoalRelationships));
|
|
3081
|
+
}
|
|
3082
|
+
if (violations.length === 0) {
|
|
3083
|
+
context.stdout("All validations passed!");
|
|
3084
|
+
return { exitCode: 0 };
|
|
3085
|
+
}
|
|
3086
|
+
context.stderr(`Found ${violations.length} violation(s):`);
|
|
3087
|
+
context.stderr("");
|
|
3088
|
+
for (const v of violations) {
|
|
3089
|
+
const location = v.line ? `:${v.line}` : "";
|
|
3090
|
+
context.stderr(` ${v.file}${location}`);
|
|
3091
|
+
context.stderr(` ${v.message}`);
|
|
1860
3092
|
}
|
|
3093
|
+
return { exitCode: 1 };
|
|
1861
3094
|
}
|
|
1862
|
-
|
|
3095
|
+
|
|
3096
|
+
// lib/cli/commands/check.ts
|
|
3097
|
+
var DEFAULT_CHECK_TIMEOUT_MS = 13000;
|
|
3098
|
+
async function runSingleCheck(check, cwd, runner) {
|
|
3099
|
+
const timeoutMs = check.timeoutMilliseconds ?? DEFAULT_CHECK_TIMEOUT_MS;
|
|
3100
|
+
const startTime = Date.now();
|
|
3101
|
+
const result = await runner.run(check.command, cwd, timeoutMs);
|
|
3102
|
+
const durationMs = Date.now() - startTime;
|
|
1863
3103
|
return {
|
|
1864
|
-
|
|
1865
|
-
|
|
3104
|
+
name: check.name,
|
|
3105
|
+
command: check.command,
|
|
3106
|
+
exitCode: result.exitCode,
|
|
3107
|
+
output: result.output,
|
|
3108
|
+
hints: check.hints,
|
|
3109
|
+
durationMs,
|
|
3110
|
+
timedOut: result.timedOut,
|
|
3111
|
+
timeoutSeconds: timeoutMs / 1000
|
|
1866
3112
|
};
|
|
1867
3113
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
spawnClaudeCode,
|
|
1872
|
-
createStdoutSink,
|
|
1873
|
-
streamEvents
|
|
1874
|
-
};
|
|
1875
|
-
async function run(prompt, options = {}, dependencies = defaultRunnerDependencies) {
|
|
1876
|
-
const isRunOptions = (opt) => ("spawnOptions" in opt) || ("onRawEvent" in opt);
|
|
1877
|
-
const spawnOptions = isRunOptions(options) ? options.spawnOptions ?? {} : options;
|
|
1878
|
-
const onRawEvent = isRunOptions(options) ? options.onRawEvent : undefined;
|
|
1879
|
-
const events = dependencies.spawnClaudeCode(prompt, spawnOptions);
|
|
1880
|
-
const sink = dependencies.createStdoutSink();
|
|
1881
|
-
await dependencies.streamEvents(events, sink, onRawEvent);
|
|
3114
|
+
async function runConfiguredChecks(checks, cwd, runner) {
|
|
3115
|
+
const promises = checks.map((check) => runSingleCheck(check, cwd, runner));
|
|
3116
|
+
return Promise.all(promises);
|
|
1882
3117
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
if (!blockedByMatch) {
|
|
1888
|
-
return [];
|
|
1889
|
-
}
|
|
1890
|
-
const section = blockedByMatch[1].trim();
|
|
1891
|
-
if (section === "(none)") {
|
|
1892
|
-
return [];
|
|
1893
|
-
}
|
|
1894
|
-
const linkPattern = /\[.*?\]\(([^)]+\.md)\)/g;
|
|
1895
|
-
const blockers = [];
|
|
1896
|
-
let match = linkPattern.exec(section);
|
|
1897
|
-
while (match !== null) {
|
|
1898
|
-
blockers.push(match[1]);
|
|
1899
|
-
match = linkPattern.exec(section);
|
|
3118
|
+
async function runConfiguredChecksSerially(checks, cwd, runner) {
|
|
3119
|
+
const results = [];
|
|
3120
|
+
for (const check of checks) {
|
|
3121
|
+
results.push(await runSingleCheck(check, cwd, runner));
|
|
1900
3122
|
}
|
|
1901
|
-
return
|
|
3123
|
+
return results;
|
|
1902
3124
|
}
|
|
1903
|
-
async function
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
3125
|
+
async function runValidationCheck(dependencies) {
|
|
3126
|
+
const outputLines = [];
|
|
3127
|
+
const bufferedContext = {
|
|
3128
|
+
cwd: dependencies.context.cwd,
|
|
3129
|
+
stdout: (msg) => outputLines.push(msg),
|
|
3130
|
+
stderr: (msg) => outputLines.push(msg)
|
|
3131
|
+
};
|
|
3132
|
+
const startTime = Date.now();
|
|
3133
|
+
const result = await lintMarkdown({
|
|
3134
|
+
...dependencies,
|
|
3135
|
+
context: bufferedContext,
|
|
3136
|
+
arguments: []
|
|
3137
|
+
});
|
|
3138
|
+
const durationMs = Date.now() - startTime;
|
|
3139
|
+
return {
|
|
3140
|
+
name: "lint markdown",
|
|
3141
|
+
command: "dust lint markdown",
|
|
3142
|
+
exitCode: result.exitCode,
|
|
3143
|
+
output: outputLines.join(`
|
|
3144
|
+
`),
|
|
3145
|
+
isBuiltIn: true,
|
|
3146
|
+
durationMs
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function displayResults(results, context) {
|
|
3150
|
+
const passed = results.filter((r) => r.exitCode === 0);
|
|
3151
|
+
const failed = results.filter((r) => r.exitCode !== 0);
|
|
3152
|
+
for (const result of results) {
|
|
3153
|
+
if (result.timedOut) {
|
|
3154
|
+
context.stdout(`✗ ${result.name} [timed out after ${result.timeoutSeconds}s]`);
|
|
3155
|
+
} else {
|
|
3156
|
+
const timing = result.durationMs !== undefined && result.durationMs >= 1000 ? ` [${(result.durationMs / 1000).toFixed(1)}s]` : "";
|
|
3157
|
+
if (result.exitCode === 0) {
|
|
3158
|
+
context.stdout(`✓ ${result.name}${timing}`);
|
|
3159
|
+
} else {
|
|
3160
|
+
context.stdout(`✗ ${result.name}${timing}`);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
1907
3163
|
}
|
|
1908
|
-
const
|
|
1909
|
-
|
|
1910
|
-
|
|
3164
|
+
for (const result of failed) {
|
|
3165
|
+
context.stdout("");
|
|
3166
|
+
context.stdout(`> ${result.command}`);
|
|
3167
|
+
if (result.timedOut) {
|
|
3168
|
+
context.stdout(`Note: This check was killed after ${result.timeoutSeconds}s. To configure a different timeout, set "timeoutMilliseconds" in the check configuration in .dust/config/settings.json`);
|
|
3169
|
+
}
|
|
3170
|
+
if (result.output.trim()) {
|
|
3171
|
+
context.stdout(result.output.trimEnd());
|
|
3172
|
+
}
|
|
3173
|
+
if (result.hints && result.hints.length > 0) {
|
|
3174
|
+
context.stdout("");
|
|
3175
|
+
context.stdout(`Hints for fixing '${result.name}':`);
|
|
3176
|
+
for (const hint of result.hints) {
|
|
3177
|
+
context.stdout(` - ${hint}`);
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
1911
3180
|
}
|
|
1912
|
-
|
|
1913
|
-
const
|
|
1914
|
-
|
|
1915
|
-
|
|
3181
|
+
context.stdout("");
|
|
3182
|
+
const indicator = failed.length > 0 ? "✗" : "✓";
|
|
3183
|
+
context.stdout(`${indicator} ${passed.length}/${results.length} checks passed`);
|
|
3184
|
+
return failed.length > 0 ? 1 : 0;
|
|
3185
|
+
}
|
|
3186
|
+
async function check(dependencies, shellRunner = defaultShellRunner) {
|
|
3187
|
+
const {
|
|
3188
|
+
arguments: commandArguments,
|
|
3189
|
+
context,
|
|
3190
|
+
fileSystem,
|
|
3191
|
+
settings
|
|
3192
|
+
} = dependencies;
|
|
3193
|
+
const serial = commandArguments.includes("--serial");
|
|
3194
|
+
if (!settings.checks || settings.checks.length === 0) {
|
|
3195
|
+
context.stderr("Error: No checks configured in .dust/config/settings.json");
|
|
3196
|
+
context.stderr("");
|
|
3197
|
+
context.stderr("Add checks to your settings.json:");
|
|
3198
|
+
context.stderr(" {");
|
|
3199
|
+
context.stderr(' "checks": [');
|
|
3200
|
+
context.stderr(' { "name": "lint", "command": "npm run lint" },');
|
|
3201
|
+
context.stderr(' { "name": "test", "command": "npm test" }');
|
|
3202
|
+
context.stderr(" ]");
|
|
3203
|
+
context.stderr(" }");
|
|
3204
|
+
return { exitCode: 1 };
|
|
1916
3205
|
}
|
|
1917
|
-
const
|
|
1918
|
-
const
|
|
1919
|
-
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
const hasIncompleteBlocker = blockers.some((blocker) => existingTasks.has(blocker));
|
|
1924
|
-
if (!hasIncompleteBlocker) {
|
|
1925
|
-
const title = extractTitle(content);
|
|
1926
|
-
const openingSentence = extractOpeningSentence(content);
|
|
1927
|
-
const relativePath = `.dust/tasks/${file}`;
|
|
1928
|
-
tasks.push({ path: relativePath, title, openingSentence });
|
|
3206
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
3207
|
+
const hasDustDir = fileSystem.exists(dustPath);
|
|
3208
|
+
if (serial) {
|
|
3209
|
+
const results2 = [];
|
|
3210
|
+
if (hasDustDir) {
|
|
3211
|
+
results2.push(await runValidationCheck(dependencies));
|
|
1929
3212
|
}
|
|
3213
|
+
const configuredResults = await runConfiguredChecksSerially(settings.checks, context.cwd, shellRunner);
|
|
3214
|
+
results2.push(...configuredResults);
|
|
3215
|
+
const exitCode2 = displayResults(results2, context);
|
|
3216
|
+
return { exitCode: exitCode2 };
|
|
1930
3217
|
}
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
3218
|
+
const checkPromises = [];
|
|
3219
|
+
if (hasDustDir) {
|
|
3220
|
+
checkPromises.push(runValidationCheck(dependencies));
|
|
3221
|
+
}
|
|
3222
|
+
checkPromises.push(runConfiguredChecks(settings.checks, context.cwd, shellRunner));
|
|
3223
|
+
const promiseResults = await Promise.all(checkPromises);
|
|
3224
|
+
const results = [];
|
|
3225
|
+
for (const result of promiseResults) {
|
|
3226
|
+
if (Array.isArray(result)) {
|
|
3227
|
+
results.push(...result);
|
|
3228
|
+
} else {
|
|
3229
|
+
results.push(result);
|
|
1943
3230
|
}
|
|
1944
|
-
context.stdout(`${colors.cyan}→ ${task.path}${colors.reset}`);
|
|
1945
|
-
context.stdout("");
|
|
1946
3231
|
}
|
|
3232
|
+
const exitCode = displayResults(results, context);
|
|
3233
|
+
return { exitCode };
|
|
1947
3234
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
3235
|
+
|
|
3236
|
+
// lib/cli/commands/focus.ts
|
|
3237
|
+
async function focus(dependencies) {
|
|
3238
|
+
const { context, settings } = dependencies;
|
|
3239
|
+
const objective = dependencies.arguments.join(" ").trim();
|
|
3240
|
+
if (!objective) {
|
|
3241
|
+
context.stderr("Error: No objective provided");
|
|
3242
|
+
context.stderr('Usage: dust focus "your objective here"');
|
|
1954
3243
|
return { exitCode: 1 };
|
|
1955
3244
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
3245
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
3246
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
3247
|
+
context.stdout(`\uD83C\uDFAF Focus: ${objective}`);
|
|
3248
|
+
context.stdout("");
|
|
3249
|
+
const steps = [];
|
|
3250
|
+
let step = 1;
|
|
3251
|
+
steps.push(`${step}. Run \`${vars.bin} check\` to verify the project is in a good state`);
|
|
3252
|
+
step++;
|
|
3253
|
+
steps.push(`${step}. Implement the task`);
|
|
3254
|
+
step++;
|
|
3255
|
+
if (!hooksInstalled) {
|
|
3256
|
+
steps.push(`${step}. Run \`${vars.bin} check\` before committing`);
|
|
3257
|
+
step++;
|
|
1958
3258
|
}
|
|
1959
|
-
|
|
3259
|
+
steps.push(`${step}. Create a single atomic commit that includes:`, " - All implementation changes", " - Deletion of the completed task file", " - Updates to any facts that changed", " - Deletion of the idea file that spawned this task (if remaining scope exists, create new ideas for it)", "", ' Use the task title as the commit message. Task titles are written in imperative form, which is the recommended style for git commit messages. Do not add prefixes like "Complete task:" - use the title directly.', "", ' Example: If the task title is "Add validation for user input", the commit message should be:', " ```", " Add validation for user input", " ```", "");
|
|
3260
|
+
step++;
|
|
3261
|
+
steps.push(`${step}. Push your commit to the remote repository`);
|
|
3262
|
+
steps.push("");
|
|
3263
|
+
steps.push("Keep your change small and focused. One task, one commit.");
|
|
3264
|
+
context.stdout(steps.join(`
|
|
3265
|
+
`));
|
|
1960
3266
|
return { exitCode: 0 };
|
|
1961
3267
|
}
|
|
1962
3268
|
|
|
1963
|
-
// lib/cli/commands/
|
|
1964
|
-
function
|
|
1965
|
-
|
|
1966
|
-
case "loop.warning":
|
|
1967
|
-
return "⚠️ WARNING: This command skips all permission checks. Only use in a sandbox environment!";
|
|
1968
|
-
case "loop.started":
|
|
1969
|
-
return `\uD83D\uDD04 Starting dust loop claude (max ${event.maxIterations} iterations)...`;
|
|
1970
|
-
case "loop.syncing":
|
|
1971
|
-
return "\uD83C\uDF0D Syncing with remote";
|
|
1972
|
-
case "loop.sync_skipped":
|
|
1973
|
-
return `Note: git pull skipped (${event.reason})`;
|
|
1974
|
-
case "loop.checking_tasks":
|
|
1975
|
-
return null;
|
|
1976
|
-
case "loop.no_tasks":
|
|
1977
|
-
return `\uD83D\uDE34 No tasks available. Sleeping...
|
|
1978
|
-
`;
|
|
1979
|
-
case "loop.tasks_found":
|
|
1980
|
-
return `✨ Found a task. Going to work!
|
|
1981
|
-
`;
|
|
1982
|
-
case "claude.started":
|
|
1983
|
-
return "\uD83E\uDD16 Starting Claude...";
|
|
1984
|
-
case "claude.ended":
|
|
1985
|
-
return event.success ? "\uD83E\uDD16 Claude session ended (success)" : `\uD83E\uDD16 Claude session ended (error: ${event.error})`;
|
|
1986
|
-
case "claude.raw_event":
|
|
1987
|
-
return null;
|
|
1988
|
-
case "loop.iteration_complete":
|
|
1989
|
-
return `\uD83D\uDCCB Completed iteration ${event.iteration}/${event.maxIterations}`;
|
|
1990
|
-
case "loop.ended":
|
|
1991
|
-
return `\uD83C\uDFC1 Reached max iterations (${event.maxIterations}). Exiting.`;
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
async function defaultPostEvent(url, payload) {
|
|
1995
|
-
await fetch(url, {
|
|
1996
|
-
method: "POST",
|
|
1997
|
-
headers: { "Content-Type": "application/json" },
|
|
1998
|
-
body: JSON.stringify(payload)
|
|
1999
|
-
});
|
|
2000
|
-
}
|
|
2001
|
-
function createDefaultDependencies() {
|
|
2002
|
-
return {
|
|
2003
|
-
spawn: nodeSpawn2,
|
|
2004
|
-
run,
|
|
2005
|
-
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
|
|
2006
|
-
postEvent: defaultPostEvent
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
function createEventPoster(eventsUrl, sessionId, postEvent, onError, getAgentSessionId) {
|
|
2010
|
-
let sequence = 0;
|
|
2011
|
-
return (event) => {
|
|
2012
|
-
if (!eventsUrl)
|
|
2013
|
-
return;
|
|
2014
|
-
sequence++;
|
|
2015
|
-
const payload = {
|
|
2016
|
-
sequence,
|
|
2017
|
-
timestamp: new Date().toISOString(),
|
|
2018
|
-
sessionId,
|
|
2019
|
-
event
|
|
2020
|
-
};
|
|
2021
|
-
if (event.type.startsWith("claude.")) {
|
|
2022
|
-
payload.agentType = "claude";
|
|
2023
|
-
const agentSessionId = getAgentSessionId?.();
|
|
2024
|
-
if (agentSessionId) {
|
|
2025
|
-
payload.agentSessionId = agentSessionId;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
postEvent(eventsUrl, payload).catch(onError);
|
|
2029
|
-
};
|
|
2030
|
-
}
|
|
2031
|
-
var SLEEP_INTERVAL_MS = 30000;
|
|
2032
|
-
var DEFAULT_MAX_ITERATIONS = 10;
|
|
2033
|
-
async function gitPull(cwd, spawn2) {
|
|
2034
|
-
return new Promise((resolve2) => {
|
|
2035
|
-
const proc = spawn2("git", ["pull"], {
|
|
2036
|
-
cwd,
|
|
2037
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2038
|
-
});
|
|
2039
|
-
let stderr = "";
|
|
2040
|
-
proc.stderr?.on("data", (data) => {
|
|
2041
|
-
stderr += data.toString();
|
|
2042
|
-
});
|
|
2043
|
-
proc.on("close", (code) => {
|
|
2044
|
-
if (code === 0) {
|
|
2045
|
-
resolve2({ success: true });
|
|
2046
|
-
} else {
|
|
2047
|
-
resolve2({ success: false, message: stderr.trim() || "git pull failed" });
|
|
2048
|
-
}
|
|
2049
|
-
});
|
|
2050
|
-
proc.on("error", (error) => {
|
|
2051
|
-
resolve2({ success: false, message: error.message });
|
|
2052
|
-
});
|
|
2053
|
-
});
|
|
3269
|
+
// lib/cli/commands/help.ts
|
|
3270
|
+
function generateHelpText(settings) {
|
|
3271
|
+
return loadTemplate("help", { bin: settings.dustCommand });
|
|
2054
3272
|
}
|
|
2055
|
-
async function
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
...dependencies.context,
|
|
2059
|
-
stdout: () => {
|
|
2060
|
-
hasOutput = true;
|
|
2061
|
-
}
|
|
2062
|
-
};
|
|
2063
|
-
await next({ ...dependencies, context: captureContext });
|
|
2064
|
-
return hasOutput;
|
|
3273
|
+
async function help(dependencies) {
|
|
3274
|
+
dependencies.context.stdout(generateHelpText(dependencies.settings));
|
|
3275
|
+
return { exitCode: 0 };
|
|
2065
3276
|
}
|
|
2066
|
-
async function runOneIteration(dependencies, loopDependencies, emit, options = {}) {
|
|
2067
|
-
const { context } = dependencies;
|
|
2068
|
-
const { spawn: spawn2, run: run2 } = loopDependencies;
|
|
2069
|
-
const { onRawEvent } = options;
|
|
2070
|
-
emit({ type: "loop.syncing" });
|
|
2071
|
-
const pullResult = await gitPull(context.cwd, spawn2);
|
|
2072
|
-
if (!pullResult.success) {
|
|
2073
|
-
emit({
|
|
2074
|
-
type: "loop.sync_skipped",
|
|
2075
|
-
reason: pullResult.message ?? "unknown error"
|
|
2076
|
-
});
|
|
2077
|
-
emit({ type: "claude.started" });
|
|
2078
|
-
const prompt = `git pull failed with the following error:
|
|
2079
3277
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
3278
|
+
// lib/cli/commands/implement-task.ts
|
|
3279
|
+
async function implementTask(dependencies) {
|
|
3280
|
+
const { context, settings } = dependencies;
|
|
3281
|
+
const hooksInstalled = await manageGitHooks(dependencies);
|
|
3282
|
+
const vars = templateVariables(settings, hooksInstalled);
|
|
3283
|
+
context.stdout(`Run \`${vars.bin} focus "<task name>"\` to set your focus and see implementation instructions.`);
|
|
3284
|
+
return { exitCode: 0 };
|
|
3285
|
+
}
|
|
2086
3286
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
onRawEvent
|
|
2096
|
-
});
|
|
2097
|
-
emit({ type: "claude.ended", success: true });
|
|
2098
|
-
return "resolved_pull_conflict";
|
|
2099
|
-
} catch (error) {
|
|
2100
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2101
|
-
context.stderr(`Claude failed to resolve git pull conflict: ${errorMessage}`);
|
|
2102
|
-
emit({ type: "claude.ended", success: false, error: errorMessage });
|
|
2103
|
-
}
|
|
3287
|
+
// lib/cli/commands/init.ts
|
|
3288
|
+
var DUST_DIRECTORIES = ["goals", "ideas", "tasks", "facts", "config"];
|
|
3289
|
+
function generateSettings(cwd, fileSystem) {
|
|
3290
|
+
const dustCommand = detectDustCommand(cwd, fileSystem);
|
|
3291
|
+
const testCommand = detectTestCommand(cwd, fileSystem);
|
|
3292
|
+
const checks = [];
|
|
3293
|
+
if (testCommand) {
|
|
3294
|
+
checks.push({ name: "test", command: testCommand });
|
|
2104
3295
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
3296
|
+
return { dustCommand, checks };
|
|
3297
|
+
}
|
|
3298
|
+
var USE_DUST_FACT = `# Use dust for planning
|
|
3299
|
+
|
|
3300
|
+
This project uses [dust](https://github.com/joshski/dust) for planning and documentation.
|
|
3301
|
+
`;
|
|
3302
|
+
async function init(dependencies) {
|
|
3303
|
+
const { context, fileSystem } = dependencies;
|
|
3304
|
+
const colors = getColors();
|
|
3305
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
3306
|
+
const dustCommand = detectDustCommand(context.cwd, fileSystem);
|
|
3307
|
+
const agentInstruction = `Always immediately run \`${dustCommand} agent\` when you start working in this repository.`;
|
|
3308
|
+
await fileSystem.mkdir(dustPath, { recursive: true });
|
|
3309
|
+
for (const dir of DUST_DIRECTORIES) {
|
|
3310
|
+
await fileSystem.mkdir(`${dustPath}/${dir}`, { recursive: true });
|
|
2110
3311
|
}
|
|
2111
|
-
|
|
2112
|
-
emit({ type: "claude.started" });
|
|
3312
|
+
let dustDirCreated = false;
|
|
2113
3313
|
try {
|
|
2114
|
-
await
|
|
2115
|
-
|
|
2116
|
-
cwd: context.cwd,
|
|
2117
|
-
dangerouslySkipPermissions: true,
|
|
2118
|
-
env: { DUST_UNATTENDED: "1" }
|
|
2119
|
-
},
|
|
2120
|
-
onRawEvent
|
|
2121
|
-
});
|
|
2122
|
-
emit({ type: "claude.ended", success: true });
|
|
2123
|
-
return "ran_claude";
|
|
3314
|
+
await fileSystem.writeFile(`${dustPath}/facts/use-dust-for-planning.md`, USE_DUST_FACT, { flag: "wx" });
|
|
3315
|
+
dustDirCreated = true;
|
|
2124
3316
|
} catch (error) {
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
return "claude_error";
|
|
3317
|
+
if (error.code !== "EEXIST") {
|
|
3318
|
+
throw error;
|
|
3319
|
+
}
|
|
2129
3320
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
3321
|
+
try {
|
|
3322
|
+
const settings = generateSettings(context.cwd, fileSystem);
|
|
3323
|
+
await fileSystem.writeFile(`${dustPath}/config/settings.json`, `${JSON.stringify(settings, null, 2)}
|
|
3324
|
+
`, { flag: "wx" });
|
|
3325
|
+
} catch (error) {
|
|
3326
|
+
if (error.code !== "EEXIST") {
|
|
3327
|
+
throw error;
|
|
3328
|
+
}
|
|
2134
3329
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
3330
|
+
if (dustDirCreated) {
|
|
3331
|
+
context.stdout(`${colors.green}✨ Initialized${colors.reset} Dust repository in ${colors.cyan}.dust/${colors.reset}`);
|
|
3332
|
+
context.stdout(`${colors.green}\uD83D\uDCC1 Created directories:${colors.reset} ${colors.dim}${DUST_DIRECTORIES.join(", ")}${colors.reset}`);
|
|
3333
|
+
context.stdout(`${colors.green}\uD83D\uDCC4 Created initial fact:${colors.reset} ${colors.cyan}.dust/facts/use-dust-for-planning.md${colors.reset}`);
|
|
3334
|
+
context.stdout(`${colors.green}⚙️ Created settings:${colors.reset} ${colors.cyan}.dust/config/settings.json${colors.reset}`);
|
|
3335
|
+
} else {
|
|
3336
|
+
context.stdout(`${colors.yellow}\uD83D\uDCE6 Note:${colors.reset} ${colors.cyan}.dust${colors.reset} directory already exists, skipping creation`);
|
|
2138
3337
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2150
|
-
context.stderr(`Event POST failed: ${message}`);
|
|
2151
|
-
}, () => agentSessionId);
|
|
2152
|
-
const emit = (event) => {
|
|
2153
|
-
const formatted = formatEvent(event);
|
|
2154
|
-
if (formatted !== null) {
|
|
2155
|
-
context.stdout(formatted);
|
|
3338
|
+
const claudeMdPath = `${context.cwd}/CLAUDE.md`;
|
|
3339
|
+
try {
|
|
3340
|
+
const claudeContent = loadTemplate("claude-md", { dustCommand });
|
|
3341
|
+
await fileSystem.writeFile(claudeMdPath, claudeContent, { flag: "wx" });
|
|
3342
|
+
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} with agent instructions`);
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
if (error.code === "EEXIST") {
|
|
3345
|
+
context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}CLAUDE.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
|
|
3346
|
+
} else {
|
|
3347
|
+
throw error;
|
|
2156
3348
|
}
|
|
2157
|
-
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
3349
|
+
}
|
|
3350
|
+
const agentsMdPath = `${context.cwd}/AGENTS.md`;
|
|
3351
|
+
try {
|
|
3352
|
+
const agentsContent = loadTemplate("agents-md", { dustCommand });
|
|
3353
|
+
await fileSystem.writeFile(agentsMdPath, agentsContent, { flag: "wx" });
|
|
3354
|
+
context.stdout(`${colors.green}\uD83D\uDCC4 Created${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} with agent instructions`);
|
|
3355
|
+
} catch (error) {
|
|
3356
|
+
if (error.code === "EEXIST") {
|
|
3357
|
+
context.stdout(`${colors.yellow}⚠️ Warning:${colors.reset} ${colors.cyan}AGENTS.md${colors.reset} already exists. Consider adding: ${colors.dim}"${agentInstruction}"${colors.reset}`);
|
|
3358
|
+
} else {
|
|
3359
|
+
throw error;
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
const runner = dustCommand.split(" ")[0];
|
|
2162
3363
|
context.stdout("");
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
3364
|
+
context.stdout(`${colors.bold}\uD83D\uDE80 Next steps:${colors.reset} Commit the changes if you are happy, then get planning!`);
|
|
3365
|
+
context.stdout("");
|
|
3366
|
+
context.stdout(`${colors.dim}If this is a new repository, you can start adding ideas or tasks right away:${colors.reset}`);
|
|
3367
|
+
context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Idea: friendly UI for non-technical users"`);
|
|
3368
|
+
context.stdout(` ${colors.cyan}>${colors.reset} ${runner} codex "Task: set up code coverage"`);
|
|
3369
|
+
context.stdout("");
|
|
3370
|
+
context.stdout(`${colors.dim}If this is an existing codebase, you might want to backfill goals and facts:${colors.reset}`);
|
|
3371
|
+
context.stdout(` ${colors.cyan}>${colors.reset} ${runner} claude "Add goals and facts based on the code in this repository"`);
|
|
3372
|
+
return { exitCode: 0 };
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
// lib/cli/commands/list.ts
|
|
3376
|
+
import { basename as basename2 } from "node:path";
|
|
3377
|
+
var VALID_TYPES = ["tasks", "ideas", "goals", "facts"];
|
|
3378
|
+
var SECTION_HEADERS = {
|
|
3379
|
+
tasks: "\uD83D\uDCCB Tasks",
|
|
3380
|
+
ideas: "\uD83D\uDCA1 Ideas",
|
|
3381
|
+
goals: "\uD83C\uDFAF Goals",
|
|
3382
|
+
facts: "\uD83D\uDCC4 Facts"
|
|
3383
|
+
};
|
|
3384
|
+
var TYPE_EXPLANATIONS = {
|
|
3385
|
+
tasks: "Tasks are detailed work plans with dependencies and completion criteria. Each task describes a specific piece of work to be done.",
|
|
3386
|
+
ideas: "Ideas are future feature notes and proposals. Ideas capture possibilities that haven't yet been refined into actionable tasks.",
|
|
3387
|
+
goals: "Goals are mission statements and guiding principles. Goals describe desired outcomes and values that inform decision-making.",
|
|
3388
|
+
facts: "Facts are current state documentation. Facts capture how things work today, providing context for agents and contributors."
|
|
3389
|
+
};
|
|
3390
|
+
async function buildGoalHierarchy(goalsPath, fileSystem) {
|
|
3391
|
+
const files = await fileSystem.readdir(goalsPath);
|
|
3392
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3393
|
+
const relationships = [];
|
|
3394
|
+
const titleMap = new Map;
|
|
3395
|
+
for (const file of mdFiles) {
|
|
3396
|
+
const filePath = `${goalsPath}/${file}`;
|
|
3397
|
+
const content = await fileSystem.readFile(filePath);
|
|
3398
|
+
relationships.push(extractGoalRelationships(filePath, content));
|
|
3399
|
+
const title = extractTitle(content) || basename2(file, ".md");
|
|
3400
|
+
titleMap.set(filePath, title);
|
|
3401
|
+
}
|
|
3402
|
+
const relMap = new Map;
|
|
3403
|
+
for (const rel of relationships) {
|
|
3404
|
+
relMap.set(rel.filePath, rel);
|
|
3405
|
+
}
|
|
3406
|
+
const rootGoals = relationships.filter((rel) => rel.parentGoals.length === 0);
|
|
3407
|
+
function buildNode(filePath) {
|
|
3408
|
+
const rel = relMap.get(filePath);
|
|
3409
|
+
const children = [];
|
|
3410
|
+
if (rel) {
|
|
3411
|
+
for (const childPath of rel.subGoals) {
|
|
3412
|
+
children.push(buildNode(childPath));
|
|
2169
3413
|
}
|
|
2170
|
-
|
|
3414
|
+
}
|
|
3415
|
+
return {
|
|
3416
|
+
filePath,
|
|
3417
|
+
title: titleMap.get(filePath) || basename2(filePath, ".md"),
|
|
3418
|
+
children
|
|
2171
3419
|
};
|
|
2172
3420
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3421
|
+
return rootGoals.map((rel) => buildNode(rel.filePath));
|
|
3422
|
+
}
|
|
3423
|
+
function renderHierarchy(nodes, output, prefix = "") {
|
|
3424
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
3425
|
+
const node = nodes[i];
|
|
3426
|
+
const isLastNode = i === nodes.length - 1;
|
|
3427
|
+
const connector = isLastNode ? "└── " : "├── ";
|
|
3428
|
+
const childPrefix = isLastNode ? " " : "│ ";
|
|
3429
|
+
output(`${prefix}${connector}${node.title}`);
|
|
3430
|
+
if (node.children.length > 0) {
|
|
3431
|
+
renderHierarchy(node.children, output, prefix + childPrefix);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
async function list(dependencies) {
|
|
3436
|
+
const { arguments: commandArguments, context, fileSystem } = dependencies;
|
|
3437
|
+
const dustPath = `${context.cwd}/.dust`;
|
|
3438
|
+
const colors = getColors();
|
|
3439
|
+
if (!fileSystem.exists(dustPath)) {
|
|
3440
|
+
context.stderr("Error: .dust directory not found");
|
|
3441
|
+
context.stderr("Run 'dust init' to initialize a Dust repository");
|
|
3442
|
+
return { exitCode: 1 };
|
|
3443
|
+
}
|
|
3444
|
+
const typesToList = commandArguments.length === 0 ? [...VALID_TYPES] : commandArguments.filter((a) => VALID_TYPES.includes(a));
|
|
3445
|
+
if (commandArguments.length > 0 && typesToList.length === 0) {
|
|
3446
|
+
context.stderr(`Invalid type: ${commandArguments[0]}`);
|
|
3447
|
+
context.stderr(`Valid types: ${VALID_TYPES.join(", ")}`);
|
|
3448
|
+
return { exitCode: 1 };
|
|
3449
|
+
}
|
|
3450
|
+
const specificTypeRequested = commandArguments.length > 0;
|
|
3451
|
+
for (const type of typesToList) {
|
|
3452
|
+
const dirPath = `${dustPath}/${type}`;
|
|
3453
|
+
const dirExists = fileSystem.exists(dirPath);
|
|
3454
|
+
const files = dirExists ? await fileSystem.readdir(dirPath) : [];
|
|
3455
|
+
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
3456
|
+
if (mdFiles.length === 0) {
|
|
3457
|
+
if (specificTypeRequested) {
|
|
3458
|
+
context.stdout(SECTION_HEADERS[type]);
|
|
3459
|
+
context.stdout("");
|
|
3460
|
+
context.stdout(TYPE_EXPLANATIONS[type]);
|
|
3461
|
+
context.stdout("");
|
|
3462
|
+
context.stdout(`No ${type} found.`);
|
|
3463
|
+
context.stdout("");
|
|
3464
|
+
}
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
context.stdout(SECTION_HEADERS[type]);
|
|
3468
|
+
context.stdout("");
|
|
3469
|
+
context.stdout(TYPE_EXPLANATIONS[type]);
|
|
3470
|
+
context.stdout("");
|
|
3471
|
+
if (type === "goals") {
|
|
3472
|
+
const hierarchy = await buildGoalHierarchy(dirPath, fileSystem);
|
|
3473
|
+
if (hierarchy.length > 0) {
|
|
3474
|
+
context.stdout(`${colors.dim}Hierarchy:${colors.reset}`);
|
|
3475
|
+
renderHierarchy(hierarchy, (line) => context.stdout(line));
|
|
3476
|
+
context.stdout("");
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
for (const file of mdFiles) {
|
|
3480
|
+
const filePath = `${dirPath}/${file}`;
|
|
3481
|
+
const content = await fileSystem.readFile(filePath);
|
|
3482
|
+
const title = extractTitle(content);
|
|
3483
|
+
const openingSentence = extractOpeningSentence(content);
|
|
3484
|
+
const relativePath = `.dust/${type}/${file}`;
|
|
3485
|
+
if (title) {
|
|
3486
|
+
context.stdout(`${colors.bold}# ${title}${colors.reset}`);
|
|
3487
|
+
} else {
|
|
3488
|
+
context.stdout(`${colors.bold}# ${file.replace(".md", "")}${colors.reset}`);
|
|
3489
|
+
}
|
|
3490
|
+
if (openingSentence) {
|
|
3491
|
+
context.stdout(`${colors.dim}${openingSentence}${colors.reset}`);
|
|
3492
|
+
}
|
|
3493
|
+
context.stdout(`${colors.cyan}→ ${relativePath}${colors.reset}`);
|
|
3494
|
+
context.stdout("");
|
|
2185
3495
|
}
|
|
2186
3496
|
}
|
|
2187
|
-
emit({ type: "loop.ended", maxIterations });
|
|
2188
3497
|
return { exitCode: 0 };
|
|
2189
3498
|
}
|
|
2190
3499
|
|
|
@@ -2385,6 +3694,8 @@ var commandRegistry = {
|
|
|
2385
3694
|
next,
|
|
2386
3695
|
check,
|
|
2387
3696
|
agent,
|
|
3697
|
+
audit,
|
|
3698
|
+
bucket,
|
|
2388
3699
|
focus,
|
|
2389
3700
|
"new task": newTask,
|
|
2390
3701
|
"new goal": newGoal,
|