@joshski/dust 0.1.100 → 0.1.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.dust/principles/actionable-errors.md +18 -0
  2. package/.dust/principles/agent-agnostic-design.md +21 -0
  3. package/.dust/principles/agent-autonomy.md +19 -0
  4. package/.dust/principles/agent-context-inference.md +19 -0
  5. package/.dust/principles/agent-specific-enhancement.md +23 -0
  6. package/.dust/principles/atomic-commits.md +15 -0
  7. package/.dust/principles/batteries-included.md +17 -0
  8. package/.dust/principles/boy-scout-rule.md +15 -0
  9. package/.dust/principles/broken-windows.md +17 -0
  10. package/.dust/principles/clarity-over-brevity.md +13 -0
  11. package/.dust/principles/co-located-tests.md +13 -0
  12. package/.dust/principles/comprehensive-assertions.md +50 -0
  13. package/.dust/principles/comprehensive-test-coverage.md +15 -0
  14. package/.dust/principles/consistent-naming.md +13 -0
  15. package/.dust/principles/context-optimised-code.md +15 -0
  16. package/.dust/principles/context-window-efficiency.md +15 -0
  17. package/.dust/principles/cross-platform-compatibility.md +19 -0
  18. package/.dust/principles/debugging-tooling.md +19 -0
  19. package/.dust/principles/decoupled-code.md +16 -0
  20. package/.dust/principles/dependency-injection.md +15 -0
  21. package/.dust/principles/design-for-testability.md +17 -0
  22. package/.dust/principles/development-traceability.md +19 -0
  23. package/.dust/principles/easy-adoption.md +17 -0
  24. package/.dust/principles/enable-flow-state.md +20 -0
  25. package/.dust/principles/environment-independent-tests.md +19 -0
  26. package/.dust/principles/exploratory-tooling.md +19 -0
  27. package/.dust/principles/fast-feedback-loops.md +15 -0
  28. package/.dust/principles/fast-feedback.md +13 -0
  29. package/.dust/principles/functional-core-imperative-shell.md +15 -0
  30. package/.dust/principles/human-ai-collaboration.md +18 -0
  31. package/.dust/principles/ideal-agent-developer-experience.md +24 -0
  32. package/.dust/principles/intuitive-directory-structure.md +13 -0
  33. package/.dust/principles/keep-unit-tests-pure.md +25 -0
  34. package/.dust/principles/lightweight-planning.md +16 -0
  35. package/.dust/principles/lint-everything.md +19 -0
  36. package/.dust/principles/maintainable-codebase.md +21 -0
  37. package/.dust/principles/make-changes-with-confidence.md +23 -0
  38. package/.dust/principles/make-the-change-easy.md +15 -0
  39. package/.dust/principles/minimal-dependencies.md +13 -0
  40. package/.dust/principles/naming-matters.md +14 -0
  41. package/.dust/principles/progressive-disclosure.md +15 -0
  42. package/.dust/principles/readable-test-data.md +48 -0
  43. package/.dust/principles/reasonably-dry.md +13 -0
  44. package/.dust/principles/repository-hygiene.md +14 -0
  45. package/.dust/principles/reproducible-checks.md +13 -0
  46. package/.dust/principles/runtime-agnostic-tests.md +13 -0
  47. package/.dust/principles/self-contained-repository.md +17 -0
  48. package/.dust/principles/self-diagnosing-tests.md +54 -0
  49. package/.dust/principles/slow-feedback-coping.md +15 -0
  50. package/.dust/principles/small-units.md +17 -0
  51. package/.dust/principles/some-big-design-up-front.md +34 -0
  52. package/.dust/principles/stop-the-line.md +13 -0
  53. package/.dust/principles/stubs-over-mocks.md +19 -0
  54. package/.dust/principles/task-first-workflow.md +13 -0
  55. package/.dust/principles/test-isolation.md +19 -0
  56. package/.dust/principles/traceable-decisions.md +13 -0
  57. package/.dust/principles/trunk-based-development.md +19 -0
  58. package/.dust/principles/unit-test-coverage.md +13 -0
  59. package/.dust/principles/unsurprising-ux.md +15 -0
  60. package/.dust/principles/vcs-independence.md +13 -0
  61. package/dist/bucket/repository.d.ts +28 -0
  62. package/dist/cli/types.d.ts +1 -0
  63. package/dist/core-principles.js +184 -0
  64. package/dist/dust.js +470 -54
  65. package/dist/lint/validators/principle-hierarchy.d.ts +0 -1
  66. package/dist/patch/index.d.ts +8 -0
  67. package/dist/patch.js +54 -1
  68. package/package.json +7 -2
package/dist/dust.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
7
7
  var require_package = __commonJS((exports, module) => {
8
8
  module.exports = {
9
9
  name: "@joshski/dust",
10
- version: "0.1.100",
10
+ version: "0.1.101",
11
11
  description: "Flow state for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
@@ -52,13 +52,18 @@ var require_package = __commonJS((exports, module) => {
52
52
  "./patch": {
53
53
  import: "./dist/patch.js",
54
54
  types: "./dist/patch/index.d.ts"
55
+ },
56
+ "./core-principles": {
57
+ import: "./dist/core-principles.js",
58
+ types: "./dist/core-principles.d.ts"
55
59
  }
56
60
  },
57
61
  files: [
58
62
  "dist",
59
63
  "bin",
60
64
  "lib/istanbul/minimal-reporter.cjs",
61
- "biome"
65
+ "biome",
66
+ ".dust/principles"
62
67
  ],
63
68
  repository: {
64
69
  type: "git",
@@ -328,12 +333,12 @@ function readEnvConfig(env) {
328
333
  }
329
334
 
330
335
  // lib/cli/wire.ts
331
- import { existsSync as existsSync2, statSync as statSync2 } from "node:fs";
336
+ import { existsSync as existsSync3, statSync as statSync3 } from "node:fs";
332
337
  import {
333
338
  chmod as chmod2,
334
339
  mkdir as mkdir2,
335
340
  readdir as readdir2,
336
- readFile as readFile3,
341
+ readFile as readFile4,
337
342
  rename,
338
343
  writeFile as writeFile2
339
344
  } from "node:fs/promises";
@@ -359,6 +364,7 @@ import { join as join2 } from "node:path";
359
364
  var KNOWN_SETTINGS_KEYS = new Set([
360
365
  "dustCommand",
361
366
  "checks",
367
+ "excludeCorePrinciples",
362
368
  "extraDirectories",
363
369
  "installCommand",
364
370
  "eventsUrl"
@@ -465,6 +471,23 @@ function validateDustEventsUrl(settings) {
465
471
  }
466
472
  return [];
467
473
  }
474
+ function validateExcludeCorePrinciples(settings) {
475
+ if (!("excludeCorePrinciples" in settings)) {
476
+ return [];
477
+ }
478
+ if (!Array.isArray(settings.excludeCorePrinciples)) {
479
+ return [{ message: '"excludeCorePrinciples" must be an array of strings' }];
480
+ }
481
+ const violations = [];
482
+ for (let i = 0;i < settings.excludeCorePrinciples.length; i++) {
483
+ if (typeof settings.excludeCorePrinciples[i] !== "string") {
484
+ violations.push({
485
+ message: `excludeCorePrinciples[${i}] must be a string`
486
+ });
487
+ }
488
+ }
489
+ return violations;
490
+ }
468
491
  function validateSettingsJson(content) {
469
492
  const violations = [];
470
493
  let parsed;
@@ -491,6 +514,7 @@ function validateSettingsJson(content) {
491
514
  }
492
515
  }
493
516
  violations.push(...validateChecksConfig(settings));
517
+ violations.push(...validateExcludeCorePrinciples(settings));
494
518
  violations.push(...validateExtraDirectories(settings));
495
519
  violations.push(...validateDustEventsUrl(settings));
496
520
  if ("dustCommand" in settings && typeof settings.dustCommand !== "string") {
@@ -639,7 +663,7 @@ async function loadSettings(cwd, fileSystem, runtime) {
639
663
  }
640
664
 
641
665
  // lib/version.ts
642
- var DUST_VERSION = "0.1.100";
666
+ var DUST_VERSION = "0.1.101";
643
667
 
644
668
  // lib/session.ts
645
669
  var DUST_UNATTENDED = "DUST_UNATTENDED";
@@ -6979,6 +7003,36 @@ function startRepositoryLoop(repoState, repoDeps, sendEvent, sessionId) {
6979
7003
  function shouldRecloneForBranchChange(existing, incoming) {
6980
7004
  return existing.branch !== incoming.branch;
6981
7005
  }
7006
+ function computeRepositoryReconciliation(existing, incoming) {
7007
+ const actions = [];
7008
+ for (const [name, repo] of incoming) {
7009
+ const existingRepo = existing.get(name);
7010
+ if (!existingRepo) {
7011
+ actions.push({ type: "add", repository: repo });
7012
+ } else if (shouldRecloneForBranchChange(existingRepo, repo)) {
7013
+ const from = existingRepo.branch ?? "(default)";
7014
+ const to = repo.branch ?? "(default)";
7015
+ actions.push({
7016
+ type: "reclone",
7017
+ name,
7018
+ repository: repo,
7019
+ reason: `branch changed from ${from} to ${to}`
7020
+ });
7021
+ } else if (existingRepo.agentProvider !== repo.agentProvider) {
7022
+ actions.push({
7023
+ type: "updateProvider",
7024
+ name,
7025
+ newProvider: repo.agentProvider
7026
+ });
7027
+ }
7028
+ }
7029
+ for (const name of existing.keys()) {
7030
+ if (!incoming.has(name)) {
7031
+ actions.push({ type: "remove", name });
7032
+ }
7033
+ }
7034
+ return actions;
7035
+ }
6982
7036
  function parseRepository(data) {
6983
7037
  if (typeof data === "object" && data !== null && "name" in data && "gitUrl" in data) {
6984
7038
  const repositoryData = data;
@@ -7070,34 +7124,46 @@ async function removeRepositoryFromManager(repoName, manager, repoDeps, context)
7070
7124
  manager.emit(removedEvent);
7071
7125
  context.stdout(formatBucketEvent(removedEvent));
7072
7126
  }
7073
- async function handleRepositoryList(repositories, manager, repoDeps, context) {
7074
- const incomingRepos = new Map;
7075
- for (const data of repositories) {
7076
- const repo = parseRepository(data);
7127
+ function parseRepositoryList(data) {
7128
+ const repos = new Map;
7129
+ for (const item of data) {
7130
+ const repo = parseRepository(item);
7077
7131
  if (repo) {
7078
- incomingRepos.set(repo.name, repo);
7079
- }
7080
- }
7081
- for (const [name, repo] of incomingRepos) {
7082
- const existing = manager.repositories.get(name);
7083
- if (!existing) {
7084
- await addRepository(repo, manager, repoDeps, context);
7085
- } else if (shouldRecloneForBranchChange(existing.repository, repo)) {
7086
- const from = existing.repository.branch ?? "(default)";
7087
- const to = repo.branch ?? "(default)";
7088
- log8(`${name}: branch changed from ${from} to ${to}, re-cloning`);
7089
- await removeRepositoryFromManager(name, manager, repoDeps, context);
7090
- await addRepository(repo, manager, repoDeps, context);
7091
- } else if (existing.repository.agentProvider !== repo.agentProvider) {
7092
- const from = existing.repository.agentProvider ?? "(unset)";
7093
- const to = repo.agentProvider ?? "(unset)";
7094
- log8(`${name}: agentProvider changed from ${from} to ${to}`);
7095
- existing.repository.agentProvider = repo.agentProvider;
7132
+ repos.set(repo.name, repo);
7096
7133
  }
7097
7134
  }
7098
- for (const name of manager.repositories.keys()) {
7099
- if (!incomingRepos.has(name)) {
7100
- await removeRepositoryFromManager(name, manager, repoDeps, context);
7135
+ return repos;
7136
+ }
7137
+ async function handleRepositoryList(repositories, manager, repoDeps, context) {
7138
+ const incoming = parseRepositoryList(repositories);
7139
+ const existing = new Map;
7140
+ for (const [name, state] of manager.repositories) {
7141
+ existing.set(name, state.repository);
7142
+ }
7143
+ const actions = computeRepositoryReconciliation(existing, incoming);
7144
+ for (const action of actions) {
7145
+ switch (action.type) {
7146
+ case "add":
7147
+ await addRepository(action.repository, manager, repoDeps, context);
7148
+ break;
7149
+ case "remove":
7150
+ await removeRepositoryFromManager(action.name, manager, repoDeps, context);
7151
+ break;
7152
+ case "reclone":
7153
+ log8(`${action.name}: ${action.reason}, re-cloning`);
7154
+ await removeRepositoryFromManager(action.name, manager, repoDeps, context);
7155
+ await addRepository(action.repository, manager, repoDeps, context);
7156
+ break;
7157
+ case "updateProvider": {
7158
+ const repoState = manager.repositories.get(action.name);
7159
+ if (!repoState)
7160
+ break;
7161
+ const from = repoState.repository.agentProvider ?? "(unset)";
7162
+ const to = action.newProvider ?? "(unset)";
7163
+ log8(`${action.name}: agentProvider changed from ${from} to ${to}`);
7164
+ repoState.repository.agentProvider = action.newProvider;
7165
+ break;
7166
+ }
7101
7167
  }
7102
7168
  }
7103
7169
  }
@@ -7199,7 +7265,7 @@ function parseRepositoryItem(r) {
7199
7265
  }
7200
7266
  return item;
7201
7267
  }
7202
- function parseRepositoryList(message) {
7268
+ function parseRepositoryList2(message) {
7203
7269
  if (!Array.isArray(message.repositories)) {
7204
7270
  return null;
7205
7271
  }
@@ -7269,7 +7335,7 @@ function parseConnectionRejected(message) {
7269
7335
  return result;
7270
7336
  }
7271
7337
  var messageParsers = {
7272
- "repository-list": parseRepositoryList,
7338
+ "repository-list": parseRepositoryList2,
7273
7339
  "task-available": parseTaskAvailable,
7274
7340
  "tool-definitions": parseToolDefinitions,
7275
7341
  "connection-ready": parseConnectionReady,
@@ -9056,6 +9122,57 @@ Run \`dust bucket tool ${toolName}\` to see available operations.`);
9056
9122
  // lib/cli/commands/lint-markdown.ts
9057
9123
  import { isAbsolute, join as join9, relative, sep } from "node:path";
9058
9124
 
9125
+ // lib/artifacts/principles.ts
9126
+ function extractLinksFromSection(content, sectionHeading) {
9127
+ const lines = content.split(`
9128
+ `);
9129
+ const links = [];
9130
+ let inSection = false;
9131
+ for (const line of lines) {
9132
+ if (line.startsWith("## ")) {
9133
+ inSection = line.trimEnd() === `## ${sectionHeading}`;
9134
+ continue;
9135
+ }
9136
+ if (!inSection)
9137
+ continue;
9138
+ if (line.startsWith("# "))
9139
+ break;
9140
+ const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
9141
+ if (linkMatch) {
9142
+ const target = linkMatch[2];
9143
+ const slugMatch = target.match(/([^/]+)\.md$/);
9144
+ if (slugMatch) {
9145
+ links.push(slugMatch[1]);
9146
+ }
9147
+ }
9148
+ }
9149
+ return links;
9150
+ }
9151
+ function extractSingleLinkFromSection(content, sectionHeading) {
9152
+ const links = extractLinksFromSection(content, sectionHeading);
9153
+ return links.length === 1 ? links[0] : null;
9154
+ }
9155
+ async function parsePrinciple(fileSystem, dustPath, slug) {
9156
+ const principlePath = `${dustPath}/principles/${slug}.md`;
9157
+ if (!fileSystem.exists(principlePath)) {
9158
+ throw new Error(`Principle not found: "${slug}" (expected file at ${principlePath})`);
9159
+ }
9160
+ const content = await fileSystem.readFile(principlePath);
9161
+ const title = extractTitle(content);
9162
+ if (!title) {
9163
+ throw new Error(`Principle file has no title: ${principlePath}`);
9164
+ }
9165
+ const parentPrinciple = extractSingleLinkFromSection(content, "Parent Principle");
9166
+ const subPrinciples = extractLinksFromSection(content, "Sub-Principles");
9167
+ return {
9168
+ slug,
9169
+ title,
9170
+ content,
9171
+ parentPrinciple,
9172
+ subPrinciples
9173
+ };
9174
+ }
9175
+
9059
9176
  // lib/artifacts/index.ts
9060
9177
  var ARTIFACT_TYPES = [
9061
9178
  "facts",
@@ -10250,7 +10367,119 @@ async function init(dependencies) {
10250
10367
  }
10251
10368
 
10252
10369
  // lib/cli/commands/list.ts
10253
- import { basename as basename3 } from "node:path";
10370
+ import { basename as basename3, resolve as resolve4 } from "node:path";
10371
+
10372
+ // lib/core-principles.ts
10373
+ import { join as join10, dirname as dirname5 } from "node:path";
10374
+ import { fileURLToPath } from "node:url";
10375
+ import { existsSync, readdirSync, statSync as statSync2 } from "node:fs";
10376
+ import { readFile as readFile3 } from "node:fs/promises";
10377
+
10378
+ // lib/artifacts/core-principles.ts
10379
+ function sortNodes(nodes) {
10380
+ nodes.sort((a, b) => a.title.localeCompare(b.title));
10381
+ for (const node of nodes) {
10382
+ sortNodes(node.children);
10383
+ }
10384
+ }
10385
+ function isInternalPrinciple(principleContent) {
10386
+ const lines = principleContent.split(`
10387
+ `);
10388
+ let inApplicabilitySection = false;
10389
+ for (const line of lines) {
10390
+ if (line.startsWith("## ")) {
10391
+ inApplicabilitySection = line.trimEnd() === "## Applicability";
10392
+ continue;
10393
+ }
10394
+ if (inApplicabilitySection) {
10395
+ if (line.startsWith("# "))
10396
+ break;
10397
+ if (line.trim() === "Internal") {
10398
+ return true;
10399
+ }
10400
+ }
10401
+ }
10402
+ return false;
10403
+ }
10404
+ function getCorePrincipleTree(allPrinciples, config) {
10405
+ const excludeSet = new Set(config.excludeCorePrinciples ?? []);
10406
+ const filteredPrinciples = allPrinciples.filter((p) => !isInternalPrinciple(p.content) && !excludeSet.has(p.slug));
10407
+ const filteredSlugs = new Set(filteredPrinciples.map((p) => p.slug));
10408
+ const nodeBySlug = new Map;
10409
+ for (const p of filteredPrinciples) {
10410
+ nodeBySlug.set(p.slug, {
10411
+ slug: p.slug,
10412
+ title: p.title,
10413
+ children: []
10414
+ });
10415
+ }
10416
+ const roots = [];
10417
+ for (const p of filteredPrinciples) {
10418
+ const node = nodeBySlug.get(p.slug);
10419
+ const parentSlug = p.parentPrinciple;
10420
+ if (!parentSlug || !filteredSlugs.has(parentSlug)) {
10421
+ roots.push(node);
10422
+ } else {
10423
+ const parentNode = nodeBySlug.get(parentSlug);
10424
+ if (parentNode) {
10425
+ parentNode.children.push(node);
10426
+ }
10427
+ }
10428
+ }
10429
+ sortNodes(roots);
10430
+ return roots;
10431
+ }
10432
+
10433
+ // lib/core-principles.ts
10434
+ function createReadableFileSystem() {
10435
+ return {
10436
+ exists: existsSync,
10437
+ isDirectory: (path2) => {
10438
+ try {
10439
+ return statSync2(path2).isDirectory();
10440
+ } catch {
10441
+ return false;
10442
+ }
10443
+ },
10444
+ readFile: (path2) => readFile3(path2, "utf-8"),
10445
+ readdir: async (path2) => readdirSync(path2)
10446
+ };
10447
+ }
10448
+ function locatePackagePrinciplesDir() {
10449
+ const thisFile = fileURLToPath(import.meta.url);
10450
+ const thisDir = dirname5(thisFile);
10451
+ const packageRoot = dirname5(thisDir);
10452
+ const principlesDir = join10(packageRoot, ".dust", "principles");
10453
+ if (!existsSync(principlesDir)) {
10454
+ throw new Error(`Core principles directory not found at ${principlesDir}. ` + "Ensure the @joshski/dust package is properly installed.");
10455
+ }
10456
+ return principlesDir;
10457
+ }
10458
+ async function readAllCorePrinciples() {
10459
+ const principlesDir = locatePackagePrinciplesDir();
10460
+ const packageRoot = dirname5(dirname5(principlesDir));
10461
+ const dustPath = join10(packageRoot, ".dust");
10462
+ const fileSystem = createReadableFileSystem();
10463
+ const files = readdirSync(principlesDir);
10464
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
10465
+ const principles = [];
10466
+ for (const file of mdFiles) {
10467
+ const slug = file.replace(/\.md$/, "");
10468
+ const principle = await parsePrinciple(fileSystem, dustPath, slug);
10469
+ principles.push(principle);
10470
+ }
10471
+ return principles;
10472
+ }
10473
+ async function getCorePrincipleHierarchy(config = {}) {
10474
+ const allPrinciples = await readAllCorePrinciples();
10475
+ return getCorePrincipleTree(allPrinciples, config);
10476
+ }
10477
+ function getCorePrinciplesPath() {
10478
+ const principlesDir = locatePackagePrinciplesDir();
10479
+ return `${principlesDir}/`;
10480
+ }
10481
+
10482
+ // lib/cli/commands/list.ts
10254
10483
  function workflowTypeToStatus(type) {
10255
10484
  switch (type) {
10256
10485
  case "refine-idea":
@@ -10301,6 +10530,24 @@ function emitListEvent(emitEvent, type, items) {
10301
10530
  });
10302
10531
  }
10303
10532
  }
10533
+ function formatPrincipleEntry(slug, openingSentence) {
10534
+ const lines = [`* ${slug}.md`];
10535
+ if (openingSentence) {
10536
+ lines.push(` ${openingSentence}`);
10537
+ }
10538
+ return lines;
10539
+ }
10540
+ function formatPrinciplesSection(header, entries) {
10541
+ if (entries.length === 0) {
10542
+ return [];
10543
+ }
10544
+ const lines = [header, ""];
10545
+ for (const entry of entries) {
10546
+ lines.push(...formatPrincipleEntry(entry.slug, entry.openingSentence));
10547
+ lines.push("");
10548
+ }
10549
+ return lines;
10550
+ }
10304
10551
  async function buildPrincipleHierarchy(principlesPath, fileSystem) {
10305
10552
  const files = await fileSystem.readdir(principlesPath);
10306
10553
  const mdFiles = files.filter((f) => f.endsWith(".md"));
@@ -10347,6 +10594,18 @@ function renderHierarchy(nodes, output, prefix = "") {
10347
10594
  }
10348
10595
  }
10349
10596
  }
10597
+ function renderCorePrincipleHierarchy(nodes, output, prefix = "") {
10598
+ for (let i = 0;i < nodes.length; i++) {
10599
+ const node = nodes[i];
10600
+ const isLastNode = i === nodes.length - 1;
10601
+ const connector = isLastNode ? "└── " : "├── ";
10602
+ const childPrefix = isLastNode ? " " : "│ ";
10603
+ output(`${prefix}${connector}${node.title}`);
10604
+ if (node.children.length > 0) {
10605
+ renderCorePrincipleHierarchy(node.children, output, prefix + childPrefix);
10606
+ }
10607
+ }
10608
+ }
10350
10609
  function parseTypesToList(commandArguments) {
10351
10610
  if (commandArguments.length === 0) {
10352
10611
  return [...ARTIFACT_TYPES];
@@ -10383,14 +10642,6 @@ function outputArtifact(parameters) {
10383
10642
  async function processListType(context) {
10384
10643
  const { type, dirPath, mdFiles, colors, fileSystem, workflowTasks } = context;
10385
10644
  const { stdout, emitEvent } = context;
10386
- if (type === "principles") {
10387
- const hierarchy = await buildPrincipleHierarchy(dirPath, fileSystem);
10388
- if (hierarchy.length > 0) {
10389
- stdout(`${colors.dim}Hierarchy:${colors.reset}`);
10390
- renderHierarchy(hierarchy, (line) => stdout(line));
10391
- stdout("");
10392
- }
10393
- }
10394
10645
  const collectedItems = [];
10395
10646
  for (const file of mdFiles) {
10396
10647
  const filePath = `${dirPath}/${file}`;
@@ -10417,6 +10668,101 @@ async function processListType(context) {
10417
10668
  emitListEvent(emitEvent, type, collectedItems);
10418
10669
  }
10419
10670
  }
10671
+ async function loadCorePrinciples(localDirPath, excludeSet) {
10672
+ const corePath = getCorePrinciplesPath().replace(/\/$/, "");
10673
+ if (resolve4(localDirPath) === resolve4(corePath)) {
10674
+ return [];
10675
+ }
10676
+ const allCorePrinciples = await readAllCorePrinciples();
10677
+ return allCorePrinciples.filter((p) => !isInternalPrinciple(p.content) && !excludeSet.has(p.slug));
10678
+ }
10679
+ async function processPrinciplesList(context) {
10680
+ const { dustPath, colors, fileSystem, stdout, emitEvent } = context;
10681
+ const { excludeCorePrinciples, tree } = context;
10682
+ const excludeSet = new Set(excludeCorePrinciples ?? []);
10683
+ const localDirPath = `${dustPath}/principles`;
10684
+ const corePrinciples = await loadCorePrinciples(localDirPath, excludeSet);
10685
+ const hasCorePrinciples = corePrinciples.length > 0;
10686
+ const localDirExists = fileSystem.exists(localDirPath);
10687
+ const localFiles = localDirExists ? await fileSystem.readdir(localDirPath) : [];
10688
+ const localMdFiles = localFiles.filter((f) => f.endsWith(".md")).toSorted();
10689
+ const hasLocalPrinciples = localMdFiles.length > 0;
10690
+ if (!hasCorePrinciples && !hasLocalPrinciples) {
10691
+ return false;
10692
+ }
10693
+ stdout(SECTION_HEADERS["principles"]);
10694
+ stdout("");
10695
+ stdout(TYPE_EXPLANATIONS["principles"]);
10696
+ stdout("");
10697
+ if (tree) {
10698
+ if (hasCorePrinciples) {
10699
+ const coreHierarchy = await getCorePrincipleHierarchy({
10700
+ excludeCorePrinciples
10701
+ });
10702
+ stdout(`${colors.bold}Core${colors.reset}`);
10703
+ renderCorePrincipleHierarchy(coreHierarchy, (line) => stdout(line));
10704
+ stdout("");
10705
+ }
10706
+ if (hasLocalPrinciples) {
10707
+ const localHierarchy = await buildPrincipleHierarchy(localDirPath, fileSystem);
10708
+ stdout(`${colors.bold}Local${colors.reset}`);
10709
+ renderHierarchy(localHierarchy, (line) => stdout(line));
10710
+ stdout("");
10711
+ const collectedItems = [];
10712
+ for (const file of localMdFiles) {
10713
+ const filePath = `${localDirPath}/${file}`;
10714
+ const content = await fileSystem.readFile(filePath);
10715
+ const title = extractTitle(content);
10716
+ const relativePath = `.dust/principles/${file}`;
10717
+ const slug = file.replace(".md", "");
10718
+ const displayTitle = title || slug;
10719
+ collectedItems.push({ path: relativePath, title: displayTitle });
10720
+ }
10721
+ if (emitEvent) {
10722
+ emitListEvent(emitEvent, "principles", collectedItems);
10723
+ }
10724
+ } else if (emitEvent) {
10725
+ emitListEvent(emitEvent, "principles", []);
10726
+ }
10727
+ } else {
10728
+ if (hasCorePrinciples) {
10729
+ const corePath = getCorePrinciplesPath();
10730
+ const coreEntries = corePrinciples.toSorted((a, b) => a.slug.localeCompare(b.slug)).map((p) => ({
10731
+ slug: p.slug,
10732
+ openingSentence: extractOpeningSentence(p.content)
10733
+ }));
10734
+ const coreLines = formatPrinciplesSection(`\uD83C\uDFAF Core Principles (${corePath})`, coreEntries);
10735
+ for (const line of coreLines) {
10736
+ stdout(line);
10737
+ }
10738
+ }
10739
+ if (hasLocalPrinciples) {
10740
+ const localEntries = [];
10741
+ const collectedItems = [];
10742
+ for (const file of localMdFiles) {
10743
+ const filePath = `${localDirPath}/${file}`;
10744
+ const content = await fileSystem.readFile(filePath);
10745
+ const title = extractTitle(content);
10746
+ const openingSentence = extractOpeningSentence(content);
10747
+ const relativePath = `.dust/principles/${file}`;
10748
+ const slug = file.replace(".md", "");
10749
+ const displayTitle = title || slug;
10750
+ localEntries.push({ slug, openingSentence });
10751
+ collectedItems.push({ path: relativePath, title: displayTitle });
10752
+ }
10753
+ const localLines = formatPrinciplesSection("\uD83C\uDFAF Local Principles (.dust/principles/)", localEntries);
10754
+ for (const line of localLines) {
10755
+ stdout(line);
10756
+ }
10757
+ if (emitEvent) {
10758
+ emitListEvent(emitEvent, "principles", collectedItems);
10759
+ }
10760
+ } else if (emitEvent) {
10761
+ emitListEvent(emitEvent, "principles", []);
10762
+ }
10763
+ }
10764
+ return true;
10765
+ }
10420
10766
  async function list(dependencies) {
10421
10767
  const {
10422
10768
  arguments: commandArguments,
@@ -10431,16 +10777,41 @@ async function list(dependencies) {
10431
10777
  context.stderr("Run 'dust init' to initialize a Dust repository");
10432
10778
  return { exitCode: 1 };
10433
10779
  }
10434
- const typesToList = parseTypesToList(commandArguments);
10435
- if (commandArguments.length > 0 && typesToList.length === 0) {
10436
- context.stderr(`Invalid type: ${commandArguments[0]}`);
10780
+ const treeFlag = commandArguments.includes("--tree");
10781
+ const argsWithoutFlags = commandArguments.filter((a) => !a.startsWith("--"));
10782
+ const typesToList = parseTypesToList(argsWithoutFlags);
10783
+ if (argsWithoutFlags.length > 0 && typesToList.length === 0) {
10784
+ context.stderr(`Invalid type: ${argsWithoutFlags[0]}`);
10437
10785
  context.stderr(`Valid types: ${ARTIFACT_TYPES.join(", ")}`);
10438
10786
  return { exitCode: 1 };
10439
10787
  }
10440
- const specificTypeRequested = commandArguments.length > 0;
10788
+ const specificTypeRequested = argsWithoutFlags.length > 0;
10441
10789
  const showTaskCreationHint = specificTypeRequested && typesToList.length === 1 && typesToList[0] === "tasks";
10442
10790
  const workflowTasks = typesToList.includes("ideas") && fileSystem.exists(dustPath) ? await findAllWorkflowTasks(fileSystem, dustPath) : null;
10443
10791
  for (const type of typesToList) {
10792
+ if (type === "principles") {
10793
+ const hasContent = await processPrinciplesList({
10794
+ dustPath,
10795
+ colors,
10796
+ fileSystem,
10797
+ stdout: context.stdout,
10798
+ emitEvent: context.emitEvent,
10799
+ excludeCorePrinciples: settings.excludeCorePrinciples,
10800
+ tree: treeFlag
10801
+ });
10802
+ if (!hasContent && specificTypeRequested) {
10803
+ context.stdout(SECTION_HEADERS[type]);
10804
+ context.stdout("");
10805
+ context.stdout(TYPE_EXPLANATIONS[type]);
10806
+ context.stdout("");
10807
+ context.stdout(`No ${type} found.`);
10808
+ context.stdout("");
10809
+ if (context.emitEvent) {
10810
+ emitListEvent(context.emitEvent, type, []);
10811
+ }
10812
+ }
10813
+ continue;
10814
+ }
10444
10815
  const dirPath = `${dustPath}/${type}`;
10445
10816
  const dirExists = fileSystem.exists(dirPath);
10446
10817
  const files = dirExists ? await fileSystem.readdir(dirPath) : [];
@@ -10483,7 +10854,7 @@ async function list(dependencies) {
10483
10854
  }
10484
10855
 
10485
10856
  // lib/loop/loop.ts
10486
- import { existsSync } from "node:fs";
10857
+ import { existsSync as existsSync2 } from "node:fs";
10487
10858
  import os3 from "node:os";
10488
10859
 
10489
10860
  // lib/loop/parse-args.ts
@@ -10549,7 +10920,7 @@ async function runLoop(dependencies, loopDependencies) {
10549
10920
  const dockerDeps = {
10550
10921
  spawn: loopDependencies.dockerDeps?.spawn ?? loopDependencies.spawn,
10551
10922
  homedir: loopDependencies.dockerDeps?.homedir ?? os3.homedir,
10552
- existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync
10923
+ existsSync: loopDependencies.dockerDeps?.existsSync ?? existsSync2
10553
10924
  };
10554
10925
  const dockerResult = await prepareDockerConfig(context.cwd, dockerDeps, onLoopEvent);
10555
10926
  if ("error" in dockerResult) {
@@ -10702,6 +11073,38 @@ async function migrate(dependencies) {
10702
11073
  return { exitCode: 0 };
10703
11074
  }
10704
11075
 
11076
+ // lib/cli/commands/new-fact.ts
11077
+ function newFactInstructions(vars) {
11078
+ const intro = vars.isClaudeCodeWeb ? "Follow these steps. Use a todo list to track your progress." : "Follow these steps:";
11079
+ return dedent`
11080
+ ## Adding a New Fact
11081
+
11082
+ Facts are current state documentation. They capture how things work today—implementation details, architectural decisions, and system behavior. Unlike principles (which are aspirational) or ideas/tasks (which are future work), facts answer "how does this work today?"
11083
+
11084
+ ${intro}
11085
+ 1. Run \`${vars.bin} facts\` to see existing facts and avoid duplication
11086
+ 2. Create a new markdown file in \`.dust/facts/\` with a descriptive kebab-case name (e.g., \`authentication-flow.md\`)
11087
+ 3. Add a title as the first line using an H1 heading (e.g., \`# Authentication Flow\`)
11088
+ 4. Write an opening sentence that summarizes the fact (this appears in \`${vars.bin} facts\` output)
11089
+ 5. Add optional body sections with additional details, examples, or context
11090
+ 6. Run \`${vars.bin} lint\` to catch any formatting issues
11091
+ 7. Create a single atomic commit with a message in the format "Add fact: <title>"
11092
+ 8. Push your commit to the remote repository
11093
+
11094
+ Facts should be:
11095
+ - **Current** - They describe how things work today, not how they should work
11096
+ - **Specific** - They document concrete implementation details
11097
+ - **Discoverable** - The opening sentence should help others find relevant facts
11098
+ `;
11099
+ }
11100
+ async function newFact(dependencies) {
11101
+ const { context, settings } = dependencies;
11102
+ const hooksInstalled = await manageGitHooks(dependencies);
11103
+ const vars = templateVariables(settings, hooksInstalled);
11104
+ context.stdout(newFactInstructions(vars));
11105
+ return { exitCode: 0 };
11106
+ }
11107
+
10705
11108
  // lib/cli/commands/new-idea.ts
10706
11109
  function newIdeaInstructions(vars) {
10707
11110
  return dedent`
@@ -11000,16 +11403,28 @@ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process
11000
11403
 
11001
11404
  // lib/cli/shared/type-list.ts
11002
11405
  async function tasks(dependencies) {
11003
- return list({ ...dependencies, arguments: ["tasks"] });
11406
+ return list({
11407
+ ...dependencies,
11408
+ arguments: ["tasks", ...dependencies.arguments]
11409
+ });
11004
11410
  }
11005
11411
  async function principles(dependencies) {
11006
- return list({ ...dependencies, arguments: ["principles"] });
11412
+ return list({
11413
+ ...dependencies,
11414
+ arguments: ["principles", ...dependencies.arguments]
11415
+ });
11007
11416
  }
11008
11417
  async function ideas(dependencies) {
11009
- return list({ ...dependencies, arguments: ["ideas"] });
11418
+ return list({
11419
+ ...dependencies,
11420
+ arguments: ["ideas", ...dependencies.arguments]
11421
+ });
11010
11422
  }
11011
11423
  async function facts(dependencies) {
11012
- return list({ ...dependencies, arguments: ["facts"] });
11424
+ return list({
11425
+ ...dependencies,
11426
+ arguments: ["facts", ...dependencies.arguments]
11427
+ });
11013
11428
  }
11014
11429
 
11015
11430
  // lib/cli/main.ts
@@ -11031,6 +11446,7 @@ var commandRegistry = {
11031
11446
  "new task": newTask,
11032
11447
  "new principle": newPrinciple,
11033
11448
  "new idea": newIdea,
11449
+ "new fact": newFact,
11034
11450
  "implement task": implementTask,
11035
11451
  "pick task": pickTask,
11036
11452
  "loop claude": loopClaude,
@@ -11134,9 +11550,9 @@ function createGlobScanner(readdirFn) {
11134
11550
  };
11135
11551
  }
11136
11552
  var defaultFileSystemPrimitives = {
11137
- existsSync: existsSync2,
11138
- statSync: statSync2,
11139
- readFile: readFile3,
11553
+ existsSync: existsSync3,
11554
+ statSync: statSync3,
11555
+ readFile: readFile4,
11140
11556
  writeFile: writeFile2,
11141
11557
  mkdir: mkdir2,
11142
11558
  readdir: readdir2,