@joshski/dust 0.1.93 → 0.1.95

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.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Centralized environment configuration.
3
+ *
4
+ * Reads all environment variables once at startup and provides typed access.
5
+ * Following the "Functional Core, Imperative Shell" principle: the shell reads
6
+ * process.env once, then the functional core receives typed configuration.
7
+ */
8
+ /**
9
+ * Logging-related environment configuration.
10
+ * - DEBUG: Pattern for stdout debug logging (comma-separated wildcards)
11
+ * - DUST_LOG_DIR: Override default log directory location
12
+ * - DUST_LOG_FILE: Inherited log file path for child processes
13
+ */
14
+ export interface LoggingConfig {
15
+ debug: string | undefined;
16
+ logDir: string | undefined;
17
+ logFile: string | undefined;
18
+ }
19
+ /**
20
+ * Dustbucket connection configuration.
21
+ * - DUST_BUCKET_HOST: Override dustbucket host for auth
22
+ * - DUST_BUCKET_TOKEN: Authentication token (takes precedence over stored credential)
23
+ * - DUST_BUCKET_AGENT_CONNECT_URL: Override WebSocket URL
24
+ */
25
+ export interface BucketConfig {
26
+ host: string | undefined;
27
+ token: string | undefined;
28
+ agentConnectUrl: string | undefined;
29
+ }
30
+ /**
31
+ * Session-related configuration for dust commands.
32
+ * - DUST_PROXY_PORT: Proxy server port
33
+ * - DUST_UNATTENDED: Whether running in unattended mode
34
+ * - DUST_SKIP_AGENT: Skip agent startup
35
+ * - DUST_REPOSITORY_ID: Repository identifier for bucket operations
36
+ * - DUST_REPOS_DIR: Override default repositories directory
37
+ */
38
+ export interface SessionConfig {
39
+ proxyPort: string | undefined;
40
+ unattended: string | undefined;
41
+ skipAgent: string | undefined;
42
+ repositoryId: string | undefined;
43
+ reposDir: string | undefined;
44
+ }
45
+ /**
46
+ * Runtime environment detection.
47
+ * - BUN_INSTALL: Whether Bun package manager is available
48
+ * - DUST_EVENTS_URL: Override events posting URL
49
+ */
50
+ export interface RuntimeConfig {
51
+ bunInstall: string | undefined;
52
+ eventsUrl: string | undefined;
53
+ }
54
+ /**
55
+ * Agent detection environment variables.
56
+ * - CLAUDECODE: Claude Code is running
57
+ * - CLAUDE_CODE_REMOTE: Claude Code Web is running
58
+ * - CODEX_HOME: Codex home directory
59
+ * - CODEX_CI: Codex CI mode
60
+ */
61
+ export interface AgentDetectionConfig {
62
+ claudeCode: string | undefined;
63
+ claudeCodeRemote: string | undefined;
64
+ codexHome: string | undefined;
65
+ codexCi: string | undefined;
66
+ }
67
+ /**
68
+ * Authentication tokens.
69
+ * - CLAUDE_CODE_OAUTH_TOKEN: OAuth token for Claude Code
70
+ * - OPENAI_API_KEY: OpenAI API key for Codex
71
+ */
72
+ export interface AuthConfig {
73
+ claudeCodeOauthToken: string | undefined;
74
+ openaiApiKey: string | undefined;
75
+ }
76
+ /**
77
+ * Testing-related configuration.
78
+ * - CLAUDE_CODE_VCR_MODE: VCR recording mode ('record' or 'replay')
79
+ */
80
+ export interface TestingConfig {
81
+ vcrMode: string | undefined;
82
+ }
83
+ /**
84
+ * Complete environment configuration.
85
+ * Captures all environment variables used by dust, organized by subsystem.
86
+ */
87
+ export interface EnvConfig {
88
+ logging: LoggingConfig;
89
+ bucket: BucketConfig;
90
+ session: SessionConfig;
91
+ runtime: RuntimeConfig;
92
+ agentDetection: AgentDetectionConfig;
93
+ auth: AuthConfig;
94
+ testing: TestingConfig;
95
+ }
96
+ /**
97
+ * Read and validate all environment variables once.
98
+ * This function should be called once at startup in the imperative shell.
99
+ *
100
+ * @param env - The environment variables object (typically process.env)
101
+ * @returns Typed environment configuration
102
+ */
103
+ export declare function readEnvConfig(env: Record<string, string | undefined>): EnvConfig;
@@ -45,6 +45,7 @@
45
45
  *
46
46
  * No external dependencies.
47
47
  */
48
+ import type { LoggingConfig } from '../env-config';
48
49
  import { type LogSink } from './sink';
49
50
  export type LogFn = (...messages: unknown[]) => void;
50
51
  export interface LoggerOptions {
@@ -66,9 +67,27 @@ export interface LoggingService {
66
67
  * encapsulated inside the returned object.
67
68
  */
68
69
  export interface LoggingServiceOptions {
70
+ /**
71
+ * Logging configuration from environment.
72
+ * Required - callers must pass configuration explicitly.
73
+ */
74
+ config: LoggingConfig;
75
+ /**
76
+ * Custom stdout writer (defaults to process.stdout.write).
77
+ */
69
78
  stdout?: (line: string) => boolean;
79
+ /**
80
+ * Current working directory for default log path resolution.
81
+ * Defaults to process.cwd().
82
+ */
83
+ cwd?: () => string;
84
+ /**
85
+ * Function to set DUST_LOG_FILE for child process inheritance.
86
+ * Defaults to setting process.env[DUST_LOG_FILE].
87
+ */
88
+ setLogFileEnv?: (path: string) => void;
70
89
  }
71
- export declare function createLoggingService(options?: LoggingServiceOptions): LoggingService;
90
+ export declare function createLoggingService(options: LoggingServiceOptions): LoggingService;
72
91
  /**
73
92
  * Activate file logging for this command. See {@link LoggingService.enableFileLogs}.
74
93
  */
package/dist/logging.js CHANGED
@@ -61,7 +61,10 @@ class FileSink {
61
61
  // lib/logging/index.ts
62
62
  var DUST_LOG_FILE = "DUST_LOG_FILE";
63
63
  function createLoggingService(options) {
64
- const writeStdout = options?.stdout ?? process.stdout.write.bind(process.stdout);
64
+ const { config } = options;
65
+ const writeStdout = options.stdout ?? process.stdout.write.bind(process.stdout);
66
+ const getCwd = options.cwd ?? (() => process.cwd());
67
+ const setLogFileEnv = options.setLogFileEnv ?? ((path) => process.env[DUST_LOG_FILE] = path);
65
68
  let patterns = null;
66
69
  let initialized = false;
67
70
  let activeFileSink = null;
@@ -70,7 +73,7 @@ function createLoggingService(options) {
70
73
  if (initialized)
71
74
  return;
72
75
  initialized = true;
73
- const parsed = parsePatterns(process.env.DEBUG);
76
+ const parsed = parsePatterns(config.debug);
74
77
  patterns = parsed.length > 0 ? parsed : null;
75
78
  }
76
79
  function getOrCreateFileSink(path) {
@@ -83,20 +86,20 @@ function createLoggingService(options) {
83
86
  }
84
87
  return {
85
88
  enableFileLogs(scope, sinkForTesting) {
86
- const existing = process.env[DUST_LOG_FILE];
87
- const logDir = process.env.DUST_LOG_DIR ?? join(process.cwd(), "log");
89
+ const existing = config.logFile;
90
+ const logDir = config.logDir ?? join(getCwd(), "log");
88
91
  const path = existing ?? join(logDir, `${scope}.log`);
89
92
  if (!existing) {
90
- process.env[DUST_LOG_FILE] = path;
93
+ setLogFileEnv(path);
91
94
  }
92
95
  activeFileSink = sinkForTesting ?? new FileSink(path);
93
96
  },
94
- createLogger(name, options2) {
97
+ createLogger(name, loggerOptions) {
95
98
  let perLoggerSink;
96
- if (options2?.file === false) {
99
+ if (loggerOptions?.file === false) {
97
100
  perLoggerSink = null;
98
- } else if (typeof options2?.file === "string") {
99
- perLoggerSink = getOrCreateFileSink(options2.file);
101
+ } else if (typeof loggerOptions?.file === "string") {
102
+ perLoggerSink = getOrCreateFileSink(loggerOptions.file);
100
103
  }
101
104
  return (...messages) => {
102
105
  init();
@@ -119,7 +122,13 @@ function createLoggingService(options) {
119
122
  }
120
123
  };
121
124
  }
122
- var defaultService = createLoggingService();
125
+ var defaultService = createLoggingService({
126
+ config: {
127
+ debug: process.env.DEBUG,
128
+ logDir: process.env.DUST_LOG_DIR,
129
+ logFile: process.env.DUST_LOG_FILE
130
+ }
131
+ });
123
132
  var enableFileLogs = defaultService.enableFileLogs.bind(defaultService);
124
133
  var createLogger = defaultService.createLogger.bind(defaultService);
125
134
  var isEnabled = defaultService.isEnabled.bind(defaultService);
@@ -0,0 +1,52 @@
1
+ export interface LoopWarningEvent {
2
+ type: 'loop.warning';
3
+ }
4
+ export interface LoopStartedEvent {
5
+ type: 'loop.started';
6
+ maxIterations: number;
7
+ agentType?: string;
8
+ }
9
+ export interface LoopSyncingEvent {
10
+ type: 'loop.syncing';
11
+ }
12
+ export interface LoopSyncSkippedEvent {
13
+ type: 'loop.sync_skipped';
14
+ reason: string;
15
+ }
16
+ export interface LoopCheckingTasksEvent {
17
+ type: 'loop.checking_tasks';
18
+ }
19
+ export interface LoopNoTasksEvent {
20
+ type: 'loop.no_tasks';
21
+ }
22
+ export interface LoopTasksFoundEvent {
23
+ type: 'loop.tasks_found';
24
+ }
25
+ export interface LoopIterationCompleteEvent {
26
+ type: 'loop.iteration_complete';
27
+ iteration: number;
28
+ maxIterations: number;
29
+ }
30
+ export interface LoopEndedEvent {
31
+ type: 'loop.ended';
32
+ maxIterations: number;
33
+ }
34
+ export interface LoopDockerDetectedEvent {
35
+ type: 'loop.docker_detected';
36
+ imageTag: string;
37
+ }
38
+ export interface LoopDockerBuildingEvent {
39
+ type: 'loop.docker_building';
40
+ imageTag: string;
41
+ }
42
+ export interface LoopDockerBuiltEvent {
43
+ type: 'loop.docker_built';
44
+ imageTag: string;
45
+ }
46
+ export interface LoopDockerErrorEvent {
47
+ type: 'loop.docker_error';
48
+ error: string;
49
+ }
50
+ export type LoopEvent = LoopWarningEvent | LoopStartedEvent | LoopSyncingEvent | LoopSyncSkippedEvent | LoopCheckingTasksEvent | LoopNoTasksEvent | LoopTasksFoundEvent | LoopIterationCompleteEvent | LoopEndedEvent | LoopDockerDetectedEvent | LoopDockerBuildingEvent | LoopDockerBuiltEvent | LoopDockerErrorEvent;
51
+ export type LoopEmitFn = (event: LoopEvent) => void;
52
+ export declare function formatLoopEvent(event: LoopEvent): string | null;
@@ -0,0 +1,9 @@
1
+ import type { spawn as nodeSpawn } from 'node:child_process';
2
+ type GitPullResult = {
3
+ success: true;
4
+ } | {
5
+ success: false;
6
+ message: string;
7
+ };
8
+ export declare function gitPull(cwd: string, spawn: typeof nodeSpawn): Promise<GitPullResult>;
9
+ export {};
@@ -0,0 +1,42 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { run as claudeRun } from '../claude/run';
3
+ import type { DockerSpawnConfig } from '../claude/types';
4
+ import type { DockerDependencies } from '../docker/docker-agent';
5
+ import { type SessionConfig } from '../env-config';
6
+ import type { CommandDependencies } from '../cli/types';
7
+ import { type InvalidTask, type UnblockedTask } from '../cli/commands/next';
8
+ import type { LoopEmitFn } from './events';
9
+ import type { PostEventFn, SendAgentEventFn } from './wire-events';
10
+ export interface LoopDependencies {
11
+ spawn: typeof nodeSpawn;
12
+ run: typeof claudeRun;
13
+ sleep: (ms: number) => Promise<void>;
14
+ postEvent: PostEventFn;
15
+ session: SessionConfig;
16
+ agentType?: string;
17
+ fetch?: typeof fetch;
18
+ /** Optional overrides for Docker dependency functions (for testing) */
19
+ dockerDeps?: Partial<DockerDependencies>;
20
+ }
21
+ export declare function createDefaultDependencies(): LoopDependencies;
22
+ type IterationResult = 'no_tasks' | 'ran_claude' | 'claude_error' | 'resolved_pull_conflict';
23
+ type LogFn = (message: string) => void;
24
+ export interface IterationOptions {
25
+ onRawEvent?: (rawEvent: Record<string, unknown>) => void;
26
+ hooksInstalled?: boolean;
27
+ signal?: AbortSignal;
28
+ logger?: LogFn;
29
+ repositoryId?: string;
30
+ /** Docker spawn config when running in Docker mode */
31
+ docker?: DockerSpawnConfig;
32
+ /** Pre-formatted tools section to inject into the prompt */
33
+ toolsSection?: string;
34
+ /** Port of the command events proxy for this iteration */
35
+ proxyPort?: number;
36
+ }
37
+ export declare function findAvailableTasks(dependencies: CommandDependencies): Promise<{
38
+ tasks: UnblockedTask[];
39
+ invalidTasks: InvalidTask[];
40
+ }>;
41
+ export declare function runOneIteration(dependencies: CommandDependencies, loopDependencies: LoopDependencies, onLoopEvent: LoopEmitFn, onAgentEvent?: SendAgentEventFn, options?: IterationOptions): Promise<IterationResult>;
42
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { AgentSessionEvent, EventMessage } from '../agent-events';
2
+ export type PostEventFn = (url: string, payload: EventMessage) => Promise<void>;
3
+ export type SendAgentEventFn = (event: AgentSessionEvent) => void;
4
+ export declare function createPostEvent(fetchFn: typeof fetch): PostEventFn;
5
+ export declare function createWireEventSender(eventsUrl: string | undefined, sessionId: string, postEvent: PostEventFn, onError: (error: unknown) => void, getAgentSessionId?: () => string | undefined, repository?: string): SendAgentEventFn;
@@ -51,6 +51,34 @@ export interface ClaudeApiProxyServer {
51
51
  port: number;
52
52
  stop: () => void;
53
53
  }
54
+ export interface ProxyRequestConfig {
55
+ url: string;
56
+ headers: Record<string, string>;
57
+ }
58
+ export declare function mergeAnthropicBetaHeader(incomingHeader: string | string[] | undefined): string;
59
+ /**
60
+ * Build the proxy request configuration for forwarding to the Anthropic API.
61
+ * Returns the upstream URL and headers with the token injected.
62
+ */
63
+ export declare function buildProxyRequest(pathname: string, search: string, token: string, incomingHeaders: Record<string, string | string[] | undefined>): ProxyRequestConfig;
64
+ /**
65
+ * Filter response headers from upstream response for forwarding to the client.
66
+ * Removes transfer-encoding as we're not chunking.
67
+ */
68
+ export declare function filterResponseHeaders(upstreamHeaders: Headers): Record<string, string>;
69
+ export interface ErrorResponse {
70
+ statusCode: number;
71
+ contentType: string;
72
+ body: string;
73
+ }
74
+ /**
75
+ * Build a 401 response for when no OAuth token is available.
76
+ */
77
+ export declare function buildNoTokenResponse(): ErrorResponse;
78
+ /**
79
+ * Build a 502 response for when the upstream request fails.
80
+ */
81
+ export declare function buildUpstreamErrorResponse(error: unknown): ErrorResponse;
54
82
  /**
55
83
  * Creates a Claude API proxy server.
56
84
  * The server accepts HTTP requests and forwards them to the Anthropic API
package/dist/session.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import type { SessionConfig } from './env-config';
1
2
  export declare const DUST_UNATTENDED = "DUST_UNATTENDED";
2
3
  export declare const DUST_SKIP_AGENT = "DUST_SKIP_AGENT";
3
4
  export declare const DUST_REPOSITORY_ID = "DUST_REPOSITORY_ID";
4
5
  export declare const DUST_PROXY_PORT = "DUST_PROXY_PORT";
5
- export declare function isUnattended(env?: Record<string, string | undefined>): boolean;
6
- export declare function buildUnattendedEnv(options?: {
6
+ export declare function isUnattended(session: SessionConfig): boolean;
7
+ export declare function buildUnattendedEnv(options: {
7
8
  repositoryId?: string;
8
9
  proxyPort?: number;
10
+ session: SessionConfig;
9
11
  }): Record<string, string>;
@@ -14,6 +14,9 @@ export interface ValidationResult {
14
14
  valid: boolean;
15
15
  violations: Violation[];
16
16
  }
17
+ export interface ValidatePatchOptions {
18
+ cwd?: string;
19
+ }
17
20
  /**
18
21
  * Validates a patch of artifact changes against existing .dust/ content.
19
22
  *
@@ -21,4 +24,4 @@ export interface ValidationResult {
21
24
  * @param dustPath - Absolute path to the .dust directory
22
25
  * @param patch - Proposed new/changed files, with paths relative to dustPath
23
26
  */
24
- export declare function validatePatch(fileSystem: ReadableFileSystem, dustPath: string, patch: ArtifactPatch): Promise<ValidationResult>;
27
+ export declare function validatePatch(fileSystem: ReadableFileSystem, dustPath: string, patch: ArtifactPatch, options?: ValidatePatchOptions): Promise<ValidationResult>;
@@ -1,3 +1,6 @@
1
+ // lib/validation/index.ts
2
+ import { relative } from "node:path";
3
+
1
4
  // lib/markdown/markdown-utilities.ts
2
5
  function extractTitle(content) {
3
6
  const match = content.match(/^#\s+(.+)$/m);
@@ -161,7 +164,8 @@ async function validateContentDirectoryFiles(dirPath, fileSystem) {
161
164
  var IDEA_TRANSITION_PREFIXES = [
162
165
  "Refine Idea: ",
163
166
  "Decompose Idea: ",
164
- "Shelve Idea: "
167
+ "Shelve Idea: ",
168
+ "Expedite Idea: "
165
169
  ];
166
170
  function titleToFilename(title) {
167
171
  return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
@@ -201,7 +205,8 @@ function validateTitleFilenameMatch(filePath, content) {
201
205
  var WORKFLOW_PREFIX_TO_SECTION = {
202
206
  "Refine Idea: ": "Refines Idea",
203
207
  "Decompose Idea: ": "Decomposes Idea",
204
- "Shelve Idea: ": "Shelves Idea"
208
+ "Shelve Idea: ": "Shelves Idea",
209
+ "Expedite Idea: ": "Expedites Idea"
205
210
  };
206
211
  function validateIdeaOpenQuestions(filePath, content) {
207
212
  const violations = [];
@@ -450,24 +455,28 @@ function validateLinks(filePath, content, fileSystem) {
450
455
  let match = linkPattern.exec(line);
451
456
  while (match) {
452
457
  const linkTarget = match[2];
453
- if (!linkTarget.startsWith("http://") && !linkTarget.startsWith("https://") && !linkTarget.startsWith("#")) {
454
- if (linkTarget.startsWith("/")) {
455
- violations.push({
456
- file: filePath,
457
- message: `Absolute link not allowed: "${linkTarget}" (use a relative path instead)`,
458
- line: i + 1
459
- });
460
- } else {
461
- const targetPath = linkTarget.split("#")[0];
462
- const resolvedPath = resolve(fileDir, targetPath);
463
- if (!fileSystem.exists(resolvedPath)) {
464
- violations.push({
465
- file: filePath,
466
- message: `Broken link: "${linkTarget}"`,
467
- line: i + 1
468
- });
469
- }
470
- }
458
+ const isExternalOrAnchorLink = linkTarget.startsWith("http://") || linkTarget.startsWith("https://") || linkTarget.startsWith("#");
459
+ if (isExternalOrAnchorLink) {
460
+ match = linkPattern.exec(line);
461
+ continue;
462
+ }
463
+ if (linkTarget.startsWith("/")) {
464
+ violations.push({
465
+ file: filePath,
466
+ message: `Absolute link not allowed: "${linkTarget}" (use a relative path instead)`,
467
+ line: i + 1
468
+ });
469
+ match = linkPattern.exec(line);
470
+ continue;
471
+ }
472
+ const targetPath = linkTarget.split("#")[0];
473
+ const resolvedPath = resolve(fileDir, targetPath);
474
+ if (!fileSystem.exists(resolvedPath)) {
475
+ violations.push({
476
+ file: filePath,
477
+ message: `Broken link: "${linkTarget}"`,
478
+ line: i + 1
479
+ });
471
480
  }
472
481
  match = linkPattern.exec(line);
473
482
  }
@@ -611,16 +620,21 @@ function extractPrincipleRelationships(filePath, content) {
611
620
  let match = linkPattern.exec(line);
612
621
  while (match) {
613
622
  const linkTarget = match[2];
614
- if (!linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://")) {
615
- const targetPath = linkTarget.split("#")[0];
616
- const resolvedPath = resolve2(fileDir, targetPath);
617
- if (resolvedPath.includes("/.dust/principles/")) {
618
- if (currentSection === "## Parent Principle") {
619
- parentPrinciples.push(resolvedPath);
620
- } else {
621
- subPrinciples.push(resolvedPath);
622
- }
623
- }
623
+ const isLocalLink = !linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://");
624
+ if (!isLocalLink) {
625
+ match = linkPattern.exec(line);
626
+ continue;
627
+ }
628
+ const targetPath = linkTarget.split("#")[0];
629
+ const resolvedPath = resolve2(fileDir, targetPath);
630
+ if (!resolvedPath.includes("/.dust/principles/")) {
631
+ match = linkPattern.exec(line);
632
+ continue;
633
+ }
634
+ if (currentSection === "## Parent Principle") {
635
+ parentPrinciples.push(resolvedPath);
636
+ } else {
637
+ subPrinciples.push(resolvedPath);
624
638
  }
625
639
  match = linkPattern.exec(line);
626
640
  }
@@ -768,7 +782,7 @@ var ALLOWED_ROOT_PATHS = [
768
782
  ].join(", ");
769
783
  function validatePatchRootEntries(fileSystem, dustPath, patch) {
770
784
  const violations = [];
771
- const sortedPaths = Object.keys(patch.files).sort();
785
+ const sortedPaths = Object.keys(patch.files).toSorted();
772
786
  const reportedUnexpectedRootDirectories = new Set;
773
787
  for (const relativePath of sortedPaths) {
774
788
  const content = patch.files[relativePath];
@@ -798,7 +812,21 @@ function validatePatchRootEntries(fileSystem, dustPath, patch) {
798
812
  }
799
813
  return violations;
800
814
  }
801
- async function validatePatch(fileSystem, dustPath, patch) {
815
+ function relativizeViolationFilePath(filePath, cwd) {
816
+ const relativePath = relative(cwd, filePath);
817
+ if (relativePath === "" || relativePath === "." || relativePath === ".." || relativePath.startsWith("../") || relativePath.startsWith("..\\")) {
818
+ return filePath;
819
+ }
820
+ return relativePath;
821
+ }
822
+ function relativizeViolations(violations, cwd) {
823
+ return violations.map((violation) => ({
824
+ ...violation,
825
+ file: relativizeViolationFilePath(violation.file, cwd)
826
+ }));
827
+ }
828
+ async function validatePatch(fileSystem, dustPath, patch, options = {}) {
829
+ const cwd = options.cwd ?? process.cwd();
802
830
  const absolutePatchFiles = new Map;
803
831
  const deletedPaths = new Set;
804
832
  for (const [relativePath, content] of Object.entries(patch.files)) {
@@ -888,7 +916,7 @@ async function validatePatch(fileSystem, dustPath, patch) {
888
916
  }
889
917
  return {
890
918
  valid: violations.length === 0,
891
- violations
919
+ violations: relativizeViolations(violations, cwd)
892
920
  };
893
921
  }
894
922
  export {
@@ -36,7 +36,7 @@ function getGapLines(fileCoverage) {
36
36
  }
37
37
  }
38
38
 
39
- const sorted = [...gapLines].sort((a, b) => a - b)
39
+ const sorted = [...gapLines].toSorted((x, y) => x - y)
40
40
  const ranges = []
41
41
  let rangeStart = null
42
42
  let rangeEnd = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,7 @@
74
74
  "istanbul-lib-report": "^3.0.1",
75
75
  "knip": "^5.60.1",
76
76
  "oxfmt": "^0.36.0",
77
- "oxlint": "^1.16.0",
77
+ "oxlint": "^1.51.0",
78
78
  "typescript": "^5.8.3",
79
79
  "vitest": "^4.0.18"
80
80
  }