@joshski/dust 0.1.111 → 0.1.113

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,17 @@
1
+ export interface TaskNode {
2
+ slug: string;
3
+ blockedBy: string[];
4
+ lastCommittedAt: string | null;
5
+ }
6
+ export interface OrderedTask<T extends TaskNode> {
7
+ node: T;
8
+ executionOrder: number;
9
+ }
10
+ /**
11
+ * Computes execution order for tasks using topological sort.
12
+ * Dependencies always trump timestamps: a blocked task never appears
13
+ * before its blockers, regardless of commit time.
14
+ * Among unblocked peers, earlier lastCommittedAt values come first;
15
+ * null values sort last.
16
+ */
17
+ export declare function computeExecutionOrder<T extends TaskNode>(nodes: T[]): OrderedTask<T>[];
@@ -0,0 +1,39 @@
1
+ // lib/execution-order.ts
2
+ function computeExecutionOrder(nodes) {
3
+ if (nodes.length === 0)
4
+ return [];
5
+ const sorted = [...nodes].toSorted((a, b) => {
6
+ if (a.lastCommittedAt === null && b.lastCommittedAt === null)
7
+ return 0;
8
+ if (a.lastCommittedAt === null)
9
+ return 1;
10
+ if (b.lastCommittedAt === null)
11
+ return -1;
12
+ return new Date(a.lastCommittedAt).getTime() - new Date(b.lastCommittedAt).getTime();
13
+ });
14
+ const result = [];
15
+ const completed = new Set;
16
+ const nodeMap = new Map(nodes.map((n) => [n.slug, n]));
17
+ while (result.length < nodes.length) {
18
+ const next = sorted.find((node) => {
19
+ if (completed.has(node.slug))
20
+ return false;
21
+ return node.blockedBy.every((slug) => completed.has(slug) || !nodeMap.has(slug));
22
+ });
23
+ if (!next) {
24
+ for (const node of sorted) {
25
+ if (!completed.has(node.slug)) {
26
+ result.push({ node, executionOrder: result.length + 1 });
27
+ completed.add(node.slug);
28
+ }
29
+ }
30
+ break;
31
+ }
32
+ result.push({ node: next, executionOrder: result.length + 1 });
33
+ completed.add(next.slug);
34
+ }
35
+ return result;
36
+ }
37
+ export {
38
+ computeExecutionOrder
39
+ };
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ParsedArtifact } from '../../artifacts/parsed-artifact';
5
5
  import type { Violation } from './types';
6
+ export declare function validateNoFrontMatter(artifact: ParsedArtifact): Violation | null;
6
7
  export declare function validateOpeningSentence(artifact: ParsedArtifact): Violation | null;
7
8
  export declare function validateOpeningSentenceLength(artifact: ParsedArtifact): Violation | null;
8
9
  export declare function validateImperativeOpeningSentence(artifact: ParsedArtifact): Violation | null;
@@ -1,6 +1,7 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
2
  import type { BoundRunFn, DockerSpawnConfig } from '../claude/types';
3
3
  import type { DockerDependencies } from '../docker/docker-agent';
4
+ import type { ContainerRuntime } from '../container/runtime';
4
5
  import { type SessionConfig } from '../env-config';
5
6
  import { type ShellRunner } from '../cli/process-runner';
6
7
  import type { CommandDependencies } from '../cli/types';
@@ -40,11 +41,14 @@ export interface IterationOptions {
40
41
  branch?: string;
41
42
  /** Trace ID for correlating events across processes */
42
43
  traceId?: string;
44
+ /** Container runtime to use for container-aware pre-flight checks */
45
+ containerRuntime?: ContainerRuntime | null;
43
46
  }
44
47
  export declare function findAvailableTasks(dependencies: CommandDependencies): Promise<{
45
48
  tasks: UnblockedTask[];
46
49
  invalidTasks: InvalidTask[];
47
50
  }>;
51
+ export declare function buildContainerShellRunner(spawnFn: typeof nodeSpawn, containerRuntime: ContainerRuntime, docker: DockerSpawnConfig): ShellRunner;
48
52
  export declare function runOneIteration(dependencies: CommandDependencies, loopDependencies: LoopDependencies, onLoopEvent: LoopEmitFn, onAgentEvent?: SendAgentEventFn, options?: IterationOptions): Promise<IterationResult>;
49
53
  export declare function buildTaskPrompt(taskPath: string, taskContent: string, instructions: string, toolsSection: string, dustCommand: string, branch?: string): string;
50
54
  export {};
package/dist/patch.js CHANGED
@@ -251,6 +251,7 @@ function validateAuditHeadings(artifact) {
251
251
  }
252
252
 
253
253
  // lib/lint/validators/content-validator.ts
254
+ var FRONT_MATTER_DELIMITER = "---";
254
255
  var REQUIRED_TASK_HEADINGS = ["Task Type", "Blocked By", "Definition of Done"];
255
256
  var ALLOWED_TASK_TYPES = new Set(VALID_TASK_TYPES);
256
257
  var MAX_OPENING_SENTENCE_LENGTH = 150;
@@ -268,6 +269,18 @@ var NON_IMPERATIVE_STARTERS = new Set([
268
269
  "you",
269
270
  "i"
270
271
  ]);
272
+ function validateNoFrontMatter(artifact) {
273
+ const firstLine = artifact.rawContent.split(`
274
+ `)[0];
275
+ if (firstLine.trim() === FRONT_MATTER_DELIMITER) {
276
+ return {
277
+ file: artifact.filePath,
278
+ line: 1,
279
+ message: "Artifact must not contain front matter. The title must be the first line."
280
+ };
281
+ }
282
+ return null;
283
+ }
271
284
  function validateOpeningSentence(artifact) {
272
285
  if (!artifact.openingSentence) {
273
286
  return {
@@ -453,6 +466,8 @@ function validateIdeaOpenQuestions(artifact) {
453
466
  const topLevelStructureMessage = "Open Questions must use `### Question?` headings and `#### Option` headings at the top level. Put supporting markdown (including lists and code blocks) under an option heading. Run `dust new idea` to see the expected format.";
454
467
  let inOpenQuestions = false;
455
468
  let currentQuestionLine = null;
469
+ let currentQuestionText = null;
470
+ let currentQuestionOptionNames = new Set;
456
471
  let inOption = false;
457
472
  let inCodeBlock = false;
458
473
  for (let i = 0;i < lines.length; i++) {
@@ -476,6 +491,8 @@ function validateIdeaOpenQuestions(artifact) {
476
491
  violations.push(...validateH2Heading(filePath, line, i + 1, inOpenQuestions, currentQuestionLine));
477
492
  inOpenQuestions = line === "## Open Questions";
478
493
  currentQuestionLine = null;
494
+ currentQuestionText = null;
495
+ currentQuestionOptionNames = new Set;
479
496
  inOption = false;
480
497
  inCodeBlock = false;
481
498
  continue;
@@ -491,6 +508,7 @@ function validateIdeaOpenQuestions(artifact) {
491
508
  line: currentQuestionLine
492
509
  });
493
510
  }
511
+ currentQuestionOptionNames = new Set;
494
512
  if (!trimmedLine.endsWith("?")) {
495
513
  violations.push({
496
514
  file: filePath,
@@ -498,12 +516,24 @@ function validateIdeaOpenQuestions(artifact) {
498
516
  line: i + 1
499
517
  });
500
518
  currentQuestionLine = null;
519
+ currentQuestionText = null;
501
520
  } else {
502
521
  currentQuestionLine = i + 1;
522
+ currentQuestionText = trimmedLine.slice(4);
503
523
  }
504
524
  continue;
505
525
  }
506
526
  if (line.startsWith("#### ")) {
527
+ const optionName = trimmedLine.slice(5);
528
+ if (currentQuestionOptionNames.has(optionName)) {
529
+ violations.push({
530
+ file: filePath,
531
+ message: `Duplicate option "${optionName}" under question "${currentQuestionText}" — each option must have a unique name`,
532
+ line: i + 1
533
+ });
534
+ } else {
535
+ currentQuestionOptionNames.add(optionName);
536
+ }
507
537
  currentQuestionLine = null;
508
538
  inOption = true;
509
539
  continue;
@@ -879,6 +909,9 @@ function validateArtifacts(context) {
879
909
  }
880
910
  for (const artifacts of Object.values(byType)) {
881
911
  for (const artifact of artifacts) {
912
+ const frontMatterViolation = validateNoFrontMatter(artifact);
913
+ if (frontMatterViolation)
914
+ violations.push(frontMatterViolation);
882
915
  const openingSentenceViolation = validateOpeningSentence(artifact);
883
916
  if (openingSentenceViolation)
884
917
  violations.push(openingSentenceViolation);
@@ -248,6 +248,7 @@ function validateAuditHeadings(artifact) {
248
248
  }
249
249
 
250
250
  // lib/lint/validators/content-validator.ts
251
+ var FRONT_MATTER_DELIMITER = "---";
251
252
  var REQUIRED_TASK_HEADINGS = ["Task Type", "Blocked By", "Definition of Done"];
252
253
  var ALLOWED_TASK_TYPES = new Set(VALID_TASK_TYPES);
253
254
  var MAX_OPENING_SENTENCE_LENGTH = 150;
@@ -265,6 +266,18 @@ var NON_IMPERATIVE_STARTERS = new Set([
265
266
  "you",
266
267
  "i"
267
268
  ]);
269
+ function validateNoFrontMatter(artifact) {
270
+ const firstLine = artifact.rawContent.split(`
271
+ `)[0];
272
+ if (firstLine.trim() === FRONT_MATTER_DELIMITER) {
273
+ return {
274
+ file: artifact.filePath,
275
+ line: 1,
276
+ message: "Artifact must not contain front matter. The title must be the first line."
277
+ };
278
+ }
279
+ return null;
280
+ }
268
281
  function validateOpeningSentence(artifact) {
269
282
  if (!artifact.openingSentence) {
270
283
  return {
@@ -450,6 +463,8 @@ function validateIdeaOpenQuestions(artifact) {
450
463
  const topLevelStructureMessage = "Open Questions must use `### Question?` headings and `#### Option` headings at the top level. Put supporting markdown (including lists and code blocks) under an option heading. Run `dust new idea` to see the expected format.";
451
464
  let inOpenQuestions = false;
452
465
  let currentQuestionLine = null;
466
+ let currentQuestionText = null;
467
+ let currentQuestionOptionNames = new Set;
453
468
  let inOption = false;
454
469
  let inCodeBlock = false;
455
470
  for (let i = 0;i < lines.length; i++) {
@@ -473,6 +488,8 @@ function validateIdeaOpenQuestions(artifact) {
473
488
  violations.push(...validateH2Heading(filePath, line, i + 1, inOpenQuestions, currentQuestionLine));
474
489
  inOpenQuestions = line === "## Open Questions";
475
490
  currentQuestionLine = null;
491
+ currentQuestionText = null;
492
+ currentQuestionOptionNames = new Set;
476
493
  inOption = false;
477
494
  inCodeBlock = false;
478
495
  continue;
@@ -488,6 +505,7 @@ function validateIdeaOpenQuestions(artifact) {
488
505
  line: currentQuestionLine
489
506
  });
490
507
  }
508
+ currentQuestionOptionNames = new Set;
491
509
  if (!trimmedLine.endsWith("?")) {
492
510
  violations.push({
493
511
  file: filePath,
@@ -495,12 +513,24 @@ function validateIdeaOpenQuestions(artifact) {
495
513
  line: i + 1
496
514
  });
497
515
  currentQuestionLine = null;
516
+ currentQuestionText = null;
498
517
  } else {
499
518
  currentQuestionLine = i + 1;
519
+ currentQuestionText = trimmedLine.slice(4);
500
520
  }
501
521
  continue;
502
522
  }
503
523
  if (line.startsWith("#### ")) {
524
+ const optionName = trimmedLine.slice(5);
525
+ if (currentQuestionOptionNames.has(optionName)) {
526
+ violations.push({
527
+ file: filePath,
528
+ message: `Duplicate option "${optionName}" under question "${currentQuestionText}" — each option must have a unique name`,
529
+ line: i + 1
530
+ });
531
+ } else {
532
+ currentQuestionOptionNames.add(optionName);
533
+ }
504
534
  currentQuestionLine = null;
505
535
  inOption = true;
506
536
  continue;
@@ -876,6 +906,9 @@ function validateArtifacts(context) {
876
906
  }
877
907
  for (const artifacts of Object.values(byType)) {
878
908
  for (const artifact of artifacts) {
909
+ const frontMatterViolation = validateNoFrontMatter(artifact);
910
+ if (frontMatterViolation)
911
+ violations.push(frontMatterViolation);
879
912
  const openingSentenceViolation = validateOpeningSentence(artifact);
880
913
  if (openingSentenceViolation)
881
914
  violations.push(openingSentenceViolation);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.111",
3
+ "version": "0.1.113",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,10 @@
49
49
  "./core-principles": {
50
50
  "import": "./dist/core-principles.js",
51
51
  "types": "./dist/core-principles.d.ts"
52
+ },
53
+ "./execution-order": {
54
+ "import": "./dist/execution-order.js",
55
+ "types": "./dist/execution-order.d.ts"
52
56
  }
53
57
  },
54
58
  "files": [