@smeltjs/core 0.3.0 → 0.5.0

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 (146) hide show
  1. package/dist/agents/guide.d.ts +68 -0
  2. package/dist/agents/guide.d.ts.map +1 -0
  3. package/dist/agents/guide.js +72 -0
  4. package/dist/agents/guide.js.map +1 -0
  5. package/dist/agents/instructions.d.ts +133 -0
  6. package/dist/agents/instructions.d.ts.map +1 -0
  7. package/dist/agents/instructions.js +212 -0
  8. package/dist/agents/instructions.js.map +1 -0
  9. package/dist/agents/lint.d.ts +154 -0
  10. package/dist/agents/lint.d.ts.map +1 -0
  11. package/dist/agents/lint.js +761 -0
  12. package/dist/agents/lint.js.map +1 -0
  13. package/dist/agents/split.d.ts +113 -0
  14. package/dist/agents/split.d.ts.map +1 -0
  15. package/dist/agents/split.js +234 -0
  16. package/dist/agents/split.js.map +1 -0
  17. package/dist/cli/agents.d.ts +39 -0
  18. package/dist/cli/agents.d.ts.map +1 -0
  19. package/dist/cli/agents.js +121 -0
  20. package/dist/cli/agents.js.map +1 -0
  21. package/dist/cli/args.d.ts +1 -0
  22. package/dist/cli/args.d.ts.map +1 -1
  23. package/dist/cli/args.js.map +1 -1
  24. package/dist/cli/bin.d.ts.map +1 -1
  25. package/dist/cli/bin.js +9 -4
  26. package/dist/cli/bin.js.map +1 -1
  27. package/dist/cli/config.d.ts +20 -0
  28. package/dist/cli/config.d.ts.map +1 -1
  29. package/dist/cli/config.js +34 -1
  30. package/dist/cli/config.js.map +1 -1
  31. package/dist/cli/doctor.d.ts +66 -0
  32. package/dist/cli/doctor.d.ts.map +1 -0
  33. package/dist/cli/doctor.js +137 -0
  34. package/dist/cli/doctor.js.map +1 -0
  35. package/dist/cli/hooks.d.ts +61 -18
  36. package/dist/cli/hooks.d.ts.map +1 -1
  37. package/dist/cli/hooks.js +178 -241
  38. package/dist/cli/hooks.js.map +1 -1
  39. package/dist/cli/init.d.ts.map +1 -1
  40. package/dist/cli/init.js +8 -14
  41. package/dist/cli/init.js.map +1 -1
  42. package/dist/cli/installed.d.ts +56 -0
  43. package/dist/cli/installed.d.ts.map +1 -0
  44. package/dist/cli/installed.js +105 -0
  45. package/dist/cli/installed.js.map +1 -0
  46. package/dist/cli/lava.d.ts +31 -0
  47. package/dist/cli/lava.d.ts.map +1 -0
  48. package/dist/cli/lava.js +82 -0
  49. package/dist/cli/lava.js.map +1 -0
  50. package/dist/cli/report.d.ts +23 -0
  51. package/dist/cli/report.d.ts.map +1 -1
  52. package/dist/cli/report.js +140 -2
  53. package/dist/cli/report.js.map +1 -1
  54. package/dist/cli/run.d.ts +5 -3
  55. package/dist/cli/run.d.ts.map +1 -1
  56. package/dist/cli/run.js +2 -1
  57. package/dist/cli/run.js.map +1 -1
  58. package/dist/cli/setup.d.ts +87 -0
  59. package/dist/cli/setup.d.ts.map +1 -0
  60. package/dist/cli/setup.js +513 -0
  61. package/dist/cli/setup.js.map +1 -0
  62. package/dist/cli/shell.d.ts +11 -4
  63. package/dist/cli/shell.d.ts.map +1 -1
  64. package/dist/cli/shell.js.map +1 -1
  65. package/dist/cli/subcommands/agents.d.ts +72 -0
  66. package/dist/cli/subcommands/agents.d.ts.map +1 -0
  67. package/dist/cli/subcommands/agents.js +163 -0
  68. package/dist/cli/subcommands/agents.js.map +1 -0
  69. package/dist/cli/subcommands/doctor.d.ts +15 -0
  70. package/dist/cli/subcommands/doctor.d.ts.map +1 -0
  71. package/dist/cli/subcommands/doctor.js +43 -0
  72. package/dist/cli/subcommands/doctor.js.map +1 -0
  73. package/dist/cli/subcommands/flags.d.ts +16 -0
  74. package/dist/cli/subcommands/flags.d.ts.map +1 -1
  75. package/dist/cli/subcommands/flags.js +40 -2
  76. package/dist/cli/subcommands/flags.js.map +1 -1
  77. package/dist/cli/subcommands/hooks.d.ts.map +1 -1
  78. package/dist/cli/subcommands/hooks.js +29 -5
  79. package/dist/cli/subcommands/hooks.js.map +1 -1
  80. package/dist/cli/subcommands/init.d.ts.map +1 -1
  81. package/dist/cli/subcommands/init.js +2 -1
  82. package/dist/cli/subcommands/init.js.map +1 -1
  83. package/dist/cli/subcommands/registry.d.ts +4 -1
  84. package/dist/cli/subcommands/registry.d.ts.map +1 -1
  85. package/dist/cli/subcommands/registry.js +9 -0
  86. package/dist/cli/subcommands/registry.js.map +1 -1
  87. package/dist/cli/subcommands/setup.d.ts +23 -0
  88. package/dist/cli/subcommands/setup.d.ts.map +1 -0
  89. package/dist/cli/subcommands/setup.js +87 -0
  90. package/dist/cli/subcommands/setup.js.map +1 -0
  91. package/dist/cli/subcommands/subcommand.d.ts +1 -1
  92. package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
  93. package/dist/cli/subcommands/subcommand.js.map +1 -1
  94. package/dist/cli/wizard.d.ts +72 -0
  95. package/dist/cli/wizard.d.ts.map +1 -0
  96. package/dist/cli/wizard.js +86 -0
  97. package/dist/cli/wizard.js.map +1 -0
  98. package/dist/harness/claude-code.d.ts.map +1 -1
  99. package/dist/harness/claude-code.js +10 -0
  100. package/dist/harness/claude-code.js.map +1 -1
  101. package/dist/harness/codex.d.ts.map +1 -1
  102. package/dist/harness/codex.js +2 -0
  103. package/dist/harness/codex.js.map +1 -1
  104. package/dist/harness/gemini.d.ts.map +1 -1
  105. package/dist/harness/gemini.js +1 -0
  106. package/dist/harness/gemini.js.map +1 -1
  107. package/dist/harness/grok.d.ts.map +1 -1
  108. package/dist/harness/grok.js +5 -1
  109. package/dist/harness/grok.js.map +1 -1
  110. package/dist/harness/hermes.d.ts.map +1 -1
  111. package/dist/harness/hermes.js +1 -0
  112. package/dist/harness/hermes.js.map +1 -1
  113. package/dist/harness/kilocode.d.ts.map +1 -1
  114. package/dist/harness/kilocode.js +1 -1
  115. package/dist/harness/kilocode.js.map +1 -1
  116. package/dist/harness/opencode.d.ts.map +1 -1
  117. package/dist/harness/opencode.js +10 -0
  118. package/dist/harness/opencode.js.map +1 -1
  119. package/dist/harness/profile.d.ts +49 -3
  120. package/dist/harness/profile.d.ts.map +1 -1
  121. package/dist/harness/profile.js +15 -0
  122. package/dist/harness/profile.js.map +1 -1
  123. package/dist/harness/registry.d.ts +55 -6
  124. package/dist/harness/registry.d.ts.map +1 -1
  125. package/dist/harness/registry.js +63 -6
  126. package/dist/harness/registry.js.map +1 -1
  127. package/dist/harness/snippet.d.ts +14 -1
  128. package/dist/harness/snippet.d.ts.map +1 -1
  129. package/dist/harness/snippet.js +18 -2
  130. package/dist/harness/snippet.js.map +1 -1
  131. package/dist/index.d.ts +22 -0
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.js +17 -0
  134. package/dist/index.js.map +1 -1
  135. package/dist/setup/recipe.d.ts +73 -0
  136. package/dist/setup/recipe.d.ts.map +1 -0
  137. package/dist/setup/recipe.js +76 -0
  138. package/dist/setup/recipe.js.map +1 -0
  139. package/dist/text/json-edit.d.ts +85 -0
  140. package/dist/text/json-edit.d.ts.map +1 -0
  141. package/dist/text/json-edit.js +290 -0
  142. package/dist/text/json-edit.js.map +1 -0
  143. package/dist/types.d.ts +0 -11
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +1 -1
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The guide `smelt agents` lints against, quoted once.
3
+ *
4
+ * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and
5
+ * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its
6
+ * thesis is a context-budget argument — the same argument smelt is built on — which is
7
+ * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every
8
+ * request, and a blob that loads on every request is exactly what this repository
9
+ * measures.**
10
+ *
11
+ * The quotes live here rather than inside each rule for the reason every other
12
+ * "written twice" in this codebase got collected: an explanation that paraphrases its
13
+ * source drifts from it silently, and a reader who cannot tell smelt's opinion from
14
+ * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus
15
+ * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's
16
+ * authority, and never the guide's advice restated as smelt's law.
17
+ *
18
+ * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's
19
+ * cited figure and is printed as a citation, never applied. The only number that can
20
+ * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See
21
+ * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a
22
+ * default.
23
+ */
24
+ /** Where every quote below comes from. Printed beside the citations, never guessed. */
25
+ export declare const GUIDE_URL = "https://www.aihero.dev/a-complete-guide-to-agents-md";
26
+ /** How the guide is named in a citation. */
27
+ export declare const GUIDE_TITLE = "the AGENTS.md guide";
28
+ /**
29
+ * The guide's own phrasing, short and attributed.
30
+ *
31
+ * Each entry is a fragment of the article quoted for the rule it grounds. They are
32
+ * deliberately brief: a lint that reprinted its source would be doing to the terminal
33
+ * what a bloated AGENTS.md does to a context window.
34
+ */
35
+ export declare const GUIDE: {
36
+ /** The thesis. Why an instruction file is a budget problem at all. */
37
+ readonly loadsEveryRequest: string;
38
+ /** The cited figure. **A citation, never a threshold** — see R2. */
39
+ readonly instructionCeiling: 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency';
40
+ /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */
41
+ readonly stalenessPoisons: 'stale information actively poisons the context';
42
+ /** Why a directory listing is the wrong thing to spend the budget on. */
43
+ readonly describeCapabilities: 'instead of documenting structure, describe capabilities';
44
+ /** The tone the guide's own example is written in. */
45
+ readonly lightTouch: "notice the light touch, no 'always', no all-caps forcing";
46
+ /** The sentence that rules out `smelt agents init` (R1). */
47
+ readonly neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md';
48
+ /** The payoff of moving style rules into a linked file. */
49
+ readonly loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript';
50
+ /** Why the sum across levels is the honest number (R8). */
51
+ readonly nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level';
52
+ /** What the root file should be once the refactor is done. */
53
+ readonly pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere';
54
+ /** The mirror advice, and the reason for it (R4). */
55
+ readonly symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way';
56
+ /** What the guide says a root file actually needs. The dogfood checklist (R9). */
57
+ readonly essentials: string;
58
+ };
59
+ /**
60
+ * `… — <the guide>: "<quote>"`, the tail every explanation ends with.
61
+ *
62
+ * One function so the attribution is spelled identically everywhere: a reader
63
+ * scanning a wall of findings must be able to tell, at a glance and without counting
64
+ * quotation marks, which half of a sentence is smelt's claim and which half is the
65
+ * guide's.
66
+ */
67
+ export declare function citing(quote: string): string;
68
+ //# sourceMappingURL=guide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide.d.ts","sourceRoot":"","sources":["../../src/agents/guide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uFAAuF;AACvF,eAAO,MAAM,SAAS,yDAAyD,CAAC;AAEhF,4CAA4C;AAC5C,eAAO,MAAM,WAAW,wBAAwB,CAAC;AAEjD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK;IAChB,sEAAsE;aACtE,iBAAiB;IAGjB,oEAAoE;aACpE,kBAAkB,EAChB,qFAAqF;IACvF,iFAAiF;aACjF,gBAAgB,EAAE,gDAAgD;IAClE,yEAAyE;aACzE,oBAAoB,EAAE,yDAAyD;IAC/E,sDAAsD;aACtD,UAAU,EAAE,0DAA0D;IACtE,4DAA4D;aAC5D,aAAa,EAAE,kEAAkE;IACjF,2DAA2D;aAC3D,gBAAgB,EAAE,6DAA6D;IAC/E,2DAA2D;aAC3D,WAAW,EAAE,2EAA2E;IACxF,8DAA8D;aAC9D,eAAe,EAAE,6DAA6D;IAC9E,qDAAqD;aACrD,aAAa,EAAE,0EAA0E;IACzF,kFAAkF;aAClF,UAAU;CAGF,CAAC;AAEX;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE5C","sourcesContent":["/**\n * The guide `smelt agents` lints against, quoted once.\n *\n * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and\n * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its\n * thesis is a context-budget argument — the same argument smelt is built on — which is\n * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every\n * request, and a blob that loads on every request is exactly what this repository\n * measures.**\n *\n * The quotes live here rather than inside each rule for the reason every other\n * \"written twice\" in this codebase got collected: an explanation that paraphrases its\n * source drifts from it silently, and a reader who cannot tell smelt's opinion from\n * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus\n * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's\n * authority, and never the guide's advice restated as smelt's law.\n *\n * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's\n * cited figure and is printed as a citation, never applied. The only number that can\n * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See\n * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a\n * default.\n */\n\n/** Where every quote below comes from. Printed beside the citations, never guessed. */\nexport const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';\n\n/** How the guide is named in a citation. */\nexport const GUIDE_TITLE = 'the AGENTS.md guide';\n\n/**\n * The guide's own phrasing, short and attributed.\n *\n * Each entry is a fragment of the article quoted for the rule it grounds. They are\n * deliberately brief: a lint that reprinted its source would be doing to the terminal\n * what a bloated AGENTS.md does to a context window.\n */\nexport const GUIDE = {\n /** The thesis. Why an instruction file is a budget problem at all. */\n loadsEveryRequest:\n 'every token in your AGENTS.md gets loaded on every single request, ' +\n 'regardless of whether it is relevant',\n /** The cited figure. **A citation, never a threshold** — see R2. */\n instructionCeiling:\n 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',\n /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */\n stalenessPoisons: 'stale information actively poisons the context',\n /** Why a directory listing is the wrong thing to spend the budget on. */\n describeCapabilities: 'instead of documenting structure, describe capabilities',\n /** The tone the guide's own example is written in. */\n lightTouch: \"notice the light touch, no 'always', no all-caps forcing\",\n /** The sentence that rules out `smelt agents init` (R1). */\n neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',\n /** The payoff of moving style rules into a linked file. */\n loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',\n /** Why the sum across levels is the honest number (R8). */\n nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',\n /** What the root file should be once the refactor is done. */\n pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',\n /** The mirror advice, and the reason for it (R4). */\n symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',\n /** What the guide says a root file actually needs. The dogfood checklist (R9). */\n essentials:\n 'a one-sentence project description, the package manager if it is not npm, and ' +\n 'the build/typecheck commands if they are non-standard — \"that\\'s honestly it\"',\n} as const;\n\n/**\n * `… — <the guide>: \"<quote>\"`, the tail every explanation ends with.\n *\n * One function so the attribution is spelled identically everywhere: a reader\n * scanning a wall of findings must be able to tell, at a glance and without counting\n * quotation marks, which half of a sentence is smelt's claim and which half is the\n * guide's.\n */\nexport function citing(quote: string): string {\n return ` — ${GUIDE_TITLE}: \"${quote}\"`;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The guide `smelt agents` lints against, quoted once.
3
+ *
4
+ * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and
5
+ * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its
6
+ * thesis is a context-budget argument — the same argument smelt is built on — which is
7
+ * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every
8
+ * request, and a blob that loads on every request is exactly what this repository
9
+ * measures.**
10
+ *
11
+ * The quotes live here rather than inside each rule for the reason every other
12
+ * "written twice" in this codebase got collected: an explanation that paraphrases its
13
+ * source drifts from it silently, and a reader who cannot tell smelt's opinion from
14
+ * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus
15
+ * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's
16
+ * authority, and never the guide's advice restated as smelt's law.
17
+ *
18
+ * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's
19
+ * cited figure and is printed as a citation, never applied. The only number that can
20
+ * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See
21
+ * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a
22
+ * default.
23
+ */
24
+ /** Where every quote below comes from. Printed beside the citations, never guessed. */
25
+ export const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';
26
+ /** How the guide is named in a citation. */
27
+ export const GUIDE_TITLE = 'the AGENTS.md guide';
28
+ /**
29
+ * The guide's own phrasing, short and attributed.
30
+ *
31
+ * Each entry is a fragment of the article quoted for the rule it grounds. They are
32
+ * deliberately brief: a lint that reprinted its source would be doing to the terminal
33
+ * what a bloated AGENTS.md does to a context window.
34
+ */
35
+ export const GUIDE = {
36
+ /** The thesis. Why an instruction file is a budget problem at all. */
37
+ loadsEveryRequest: 'every token in your AGENTS.md gets loaded on every single request, ' +
38
+ 'regardless of whether it is relevant',
39
+ /** The cited figure. **A citation, never a threshold** — see R2. */
40
+ instructionCeiling: 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',
41
+ /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */
42
+ stalenessPoisons: 'stale information actively poisons the context',
43
+ /** Why a directory listing is the wrong thing to spend the budget on. */
44
+ describeCapabilities: 'instead of documenting structure, describe capabilities',
45
+ /** The tone the guide's own example is written in. */
46
+ lightTouch: "notice the light touch, no 'always', no all-caps forcing",
47
+ /** The sentence that rules out `smelt agents init` (R1). */
48
+ neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',
49
+ /** The payoff of moving style rules into a linked file. */
50
+ loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',
51
+ /** Why the sum across levels is the honest number (R8). */
52
+ nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',
53
+ /** What the root file should be once the refactor is done. */
54
+ pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',
55
+ /** The mirror advice, and the reason for it (R4). */
56
+ symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',
57
+ /** What the guide says a root file actually needs. The dogfood checklist (R9). */
58
+ essentials: 'a one-sentence project description, the package manager if it is not npm, and ' +
59
+ 'the build/typecheck commands if they are non-standard — "that\'s honestly it"',
60
+ };
61
+ /**
62
+ * `… — <the guide>: "<quote>"`, the tail every explanation ends with.
63
+ *
64
+ * One function so the attribution is spelled identically everywhere: a reader
65
+ * scanning a wall of findings must be able to tell, at a glance and without counting
66
+ * quotation marks, which half of a sentence is smelt's claim and which half is the
67
+ * guide's.
68
+ */
69
+ export function citing(quote) {
70
+ return ` — ${GUIDE_TITLE}: "${quote}"`;
71
+ }
72
+ //# sourceMappingURL=guide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide.js","sourceRoot":"","sources":["../../src/agents/guide.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAEhF,4CAA4C;AAC5C,MAAM,CAAC,MAAM,WAAW,GAAG,qBAAqB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,sEAAsE;IACtE,iBAAiB,EACf,qEAAqE;QACrE,sCAAsC;IACxC,oEAAoE;IACpE,kBAAkB,EAChB,qFAAqF;IACvF,iFAAiF;IACjF,gBAAgB,EAAE,gDAAgD;IAClE,yEAAyE;IACzE,oBAAoB,EAAE,yDAAyD;IAC/E,sDAAsD;IACtD,UAAU,EAAE,0DAA0D;IACtE,4DAA4D;IAC5D,aAAa,EAAE,kEAAkE;IACjF,2DAA2D;IAC3D,gBAAgB,EAAE,6DAA6D;IAC/E,2DAA2D;IAC3D,WAAW,EAAE,2EAA2E;IACxF,8DAA8D;IAC9D,eAAe,EAAE,6DAA6D;IAC9E,qDAAqD;IACrD,aAAa,EAAE,0EAA0E;IACzF,kFAAkF;IAClF,UAAU,EACR,gFAAgF;QAChF,+EAA+E;CACzE,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,MAAM,WAAW,MAAM,KAAK,GAAG,CAAC;AACzC,CAAC","sourcesContent":["/**\n * The guide `smelt agents` lints against, quoted once.\n *\n * Every rule in `./lint.ts` and every sentence `./split.ts` prints cites a source, and\n * the source is one article: *A Complete Guide to AGENTS.md* ({@link GUIDE_URL}). Its\n * thesis is a context-budget argument — the same argument smelt is built on — which is\n * why smelt can lint against it honestly: **an AGENTS.md is a blob that loads on every\n * request, and a blob that loads on every request is exactly what this repository\n * measures.**\n *\n * The quotes live here rather than inside each rule for the reason every other\n * \"written twice\" in this codebase got collected: an explanation that paraphrases its\n * source drifts from it silently, and a reader who cannot tell smelt's opinion from\n * the guide's cannot judge either. So a rule's explanation is *smelt's* sentence plus\n * a phrase from {@link GUIDE}, attributed — never smelt's opinion wearing the guide's\n * authority, and never the guide's advice restated as smelt's law.\n *\n * **What is not here: a threshold.** {@link GUIDE.instructionCeiling} is the guide's\n * cited figure and is printed as a citation, never applied. The only number that can\n * fail a lint is `agents.budgetBytes` in `smelt.config.json` — the user's own. See\n * ruling R2 in ISSUES.md, which is the same ruling that keeps `--budget` without a\n * default.\n */\n\n/** Where every quote below comes from. Printed beside the citations, never guessed. */\nexport const GUIDE_URL = 'https://www.aihero.dev/a-complete-guide-to-agents-md';\n\n/** How the guide is named in a citation. */\nexport const GUIDE_TITLE = 'the AGENTS.md guide';\n\n/**\n * The guide's own phrasing, short and attributed.\n *\n * Each entry is a fragment of the article quoted for the rule it grounds. They are\n * deliberately brief: a lint that reprinted its source would be doing to the terminal\n * what a bloated AGENTS.md does to a context window.\n */\nexport const GUIDE = {\n /** The thesis. Why an instruction file is a budget problem at all. */\n loadsEveryRequest:\n 'every token in your AGENTS.md gets loaded on every single request, ' +\n 'regardless of whether it is relevant',\n /** The cited figure. **A citation, never a threshold** — see R2. */\n instructionCeiling:\n 'frontier thinking LLMs can follow ~150-200 instructions with reasonable consistency',\n /** Why a dead path is worse than no path. The flagship rule's grounding (R3). */\n stalenessPoisons: 'stale information actively poisons the context',\n /** Why a directory listing is the wrong thing to spend the budget on. */\n describeCapabilities: 'instead of documenting structure, describe capabilities',\n /** The tone the guide's own example is written in. */\n lightTouch: \"notice the light touch, no 'always', no all-caps forcing\",\n /** The sentence that rules out `smelt agents init` (R1). */\n neverGenerate: 'never use initialization scripts to auto-generate your AGENTS.md',\n /** The payoff of moving style rules into a linked file. */\n loadWhenRelevant: 'TypeScript rules only load when the agent writes TypeScript',\n /** Why the sum across levels is the honest number (R8). */\n nestedMerge: 'a nested AGENTS.md merges with the root level — do not overload any level',\n /** What the root file should be once the refactor is done. */\n pointsElsewhere: 'the ideal root file is small, focused, and points elsewhere',\n /** The mirror advice, and the reason for it (R4). */\n symlinkMirror: '`ln -s AGENTS.md CLAUDE.md`, to keep all your tools working the same way',\n /** What the guide says a root file actually needs. The dogfood checklist (R9). */\n essentials:\n 'a one-sentence project description, the package manager if it is not npm, and ' +\n 'the build/typecheck commands if they are non-standard — \"that\\'s honestly it\"',\n} as const;\n\n/**\n * `… — <the guide>: \"<quote>\"`, the tail every explanation ends with.\n *\n * One function so the attribution is spelled identically everywhere: a reader\n * scanning a wall of findings must be able to tell, at a glance and without counting\n * quotation marks, which half of a sentence is smelt's claim and which half is the\n * guide's.\n */\nexport function citing(quote: string): string {\n return ` — ${GUIDE_TITLE}: \"${quote}\"`;\n}\n"]}
@@ -0,0 +1,133 @@
1
+ import type { RepoReader } from '../repomap/reader.ts';
2
+ /**
3
+ * The instruction files an agent actually loads, found through the read-only
4
+ * {@link RepoReader} seam.
5
+ *
6
+ * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule
7
+ * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so
8
+ * a lint that reported only the root file would be reporting a fraction and calling it
9
+ * the total, which is this repository's Law 4 pointed at its own output.
10
+ *
11
+ * **But a merge runs up the tree, not across it.** Two numbers come out of that, and
12
+ * they are different numbers in any monorepo, so both are reported and each is labelled
13
+ * with the question it answers:
14
+ *
15
+ * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level
16
+ * plus its **ancestors**. An agent working in `pkg/a` loads the root file and
17
+ * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the
18
+ * per-request cost, and it is the number the guide's argument is about.
19
+ * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole
20
+ * repository's instruction surface. Useful as the size of what a team maintains,
21
+ * and **not** a per-request cost. Calling it one would be the same over-count this
22
+ * module refuses for mirrors one paragraph down.
23
+ *
24
+ * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads
25
+ * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so
26
+ * summing all three would triple a cost nobody pays. At each directory the primary is
27
+ * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever
28
+ * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted
29
+ * for drift and not for bytes.
30
+ *
31
+ * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:
32
+ * that a nested file was found, that an ignored directory was never entered, that a
33
+ * path in the prose resolves against the real tree. Those are claims about calls, so
34
+ * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),
35
+ * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing
36
+ * changes for a caller who does not care.
37
+ */
38
+ /** The three file names a coding agent reads instructions from, in precedence order. */
39
+ export declare const INSTRUCTION_FILE_NAMES: readonly ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
40
+ /** One of the three names above. */
41
+ export type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];
42
+ /** True for a name this module treats as an instruction file. */
43
+ export declare function isInstructionFileName(name: string): name is InstructionFileName;
44
+ /** One instruction file on disk, read. */
45
+ export interface InstructionFile {
46
+ /** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */
47
+ readonly path: string;
48
+ readonly name: InstructionFileName;
49
+ /** The directory it sits in, root-relative; `''` for the repository root. */
50
+ readonly dir: string;
51
+ readonly text: string;
52
+ /** UTF-8 bytes. The unit every smelt budget is in. */
53
+ readonly bytes: number;
54
+ /**
55
+ * True when the entry is a symlink — the arrangement the guide recommends for
56
+ * mirrors, and the one arrangement in which a mirror **cannot** drift.
57
+ */
58
+ readonly symlink: boolean;
59
+ }
60
+ /**
61
+ * One directory's contribution: the file that level costs, plus the mirrors of it.
62
+ *
63
+ * `primary` is the only one whose bytes are summed. A mirror that differs from it is
64
+ * `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason
65
+ * the guide suggests one.
66
+ */
67
+ export interface InstructionLevel {
68
+ /** Root-relative directory; `''` is the root level. */
69
+ readonly dir: string;
70
+ readonly primary: InstructionFile;
71
+ readonly mirrors: readonly InstructionFile[];
72
+ }
73
+ /** Every instruction file in a tree, arranged by level. */
74
+ export interface InstructionSet {
75
+ /** The directory the walk started from, as the caller spelled it. */
76
+ readonly root: string;
77
+ /** Root level first, then nested levels in walk order. */
78
+ readonly levels: readonly InstructionLevel[];
79
+ /**
80
+ * The repository-wide instruction surface: every level's primary, summed.
81
+ *
82
+ * **Not a per-request cost.** Sibling levels never merge, so no single request ever
83
+ * loads all of these — see {@link perRequestBytes} for the number that answers that
84
+ * question.
85
+ */
86
+ readonly totalBytes: number;
87
+ /**
88
+ * What the most expensive single request loads: the heaviest level plus every level
89
+ * above it, since a nested file merges with its **ancestors** and with nothing else.
90
+ *
91
+ * Equal to {@link totalBytes} in a repository with one chain of levels, and strictly
92
+ * smaller as soon as two siblings both carry an instruction file.
93
+ */
94
+ readonly perRequestBytes: number;
95
+ }
96
+ /**
97
+ * Find and read every instruction file under `root`.
98
+ *
99
+ * The ignore list is the repo map's, and for the same reason: a vendored
100
+ * `node_modules/**\/AGENTS.md` is not this repository's instruction to an agent, and
101
+ * counting it would make the total a number about somebody else's project. A caller
102
+ * may replace the list wholesale — a default nobody can turn off is not a default.
103
+ *
104
+ * Symlinked *directories* are refused outright, exactly as the map refuses them: a
105
+ * walk that follows one can leave the root. A symlinked instruction **file** is read
106
+ * (following it is the point — that is what the harness does too) and flagged.
107
+ */
108
+ export declare function readInstructionSet(options: {
109
+ readonly root: string;
110
+ readonly reader?: RepoReader;
111
+ readonly ignore?: readonly string[];
112
+ }): InstructionSet;
113
+ /**
114
+ * The directories a level merges *with*, root first and nearest last — and never the
115
+ * level itself.
116
+ *
117
+ * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}
118
+ * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.
119
+ */
120
+ export declare function ancestorDirs(dir: string): readonly string[];
121
+ /**
122
+ * Does `token`, read as a path relative to the repository root, name something that
123
+ * exists? The one question `dead-path` and `dead-link` both ask.
124
+ *
125
+ * Through the reader, so the resolution is part of the same injectable walk the
126
+ * discovery is — a fixture repo can therefore contain a path that resolves and one
127
+ * that does not without either of them existing on the machine running the tests.
128
+ * A reader that throws (a malformed path, a permission error) answers "no": the
129
+ * finding is advisory, and a lint that failed the whole run because one token in
130
+ * somebody's prose was unstattable would be worse than the staleness it looks for.
131
+ */
132
+ export declare function resolvesInTree(root: string, reader: RepoReader, token: string): boolean;
133
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/agents/instructions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,wFAAwF;AACxF,eAAO,MAAM,sBAAsB,YAAI,WAAW,EAAE,WAAW,EAAE,WAAW,CAAU,CAAC;AAEvF,oCAAoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,mBAAmB,CAE/E;AAED,0CAA0C;AAC1C,MAAM,WAAW,eAAe;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;CAC9C;AAED,2DAA2D;AAC3D,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC,GAAG,cAAc,CAgFjB;AAsBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAM3D;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAMvF","sourcesContent":["import { join } from 'node:path';\n\nimport { DEFAULT_REPO_IGNORE } from '../repomap/map.ts';\nimport { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\n\n/**\n * The instruction files an agent actually loads, found through the read-only\n * {@link RepoReader} seam.\n *\n * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule\n * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so\n * a lint that reported only the root file would be reporting a fraction and calling it\n * the total, which is this repository's Law 4 pointed at its own output.\n *\n * **But a merge runs up the tree, not across it.** Two numbers come out of that, and\n * they are different numbers in any monorepo, so both are reported and each is labelled\n * with the question it answers:\n *\n * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level\n * plus its **ancestors**. An agent working in `pkg/a` loads the root file and\n * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the\n * per-request cost, and it is the number the guide's argument is about.\n * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole\n * repository's instruction surface. Useful as the size of what a team maintains,\n * and **not** a per-request cost. Calling it one would be the same over-count this\n * module refuses for mirrors one paragraph down.\n *\n * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads\n * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so\n * summing all three would triple a cost nobody pays. At each directory the primary is\n * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever\n * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted\n * for drift and not for bytes.\n *\n * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:\n * that a nested file was found, that an ignored directory was never entered, that a\n * path in the prose resolves against the real tree. Those are claims about calls, so\n * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),\n * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing\n * changes for a caller who does not care.\n */\n\n/** The three file names a coding agent reads instructions from, in precedence order. */\nexport const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'] as const;\n\n/** One of the three names above. */\nexport type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];\n\n/** True for a name this module treats as an instruction file. */\nexport function isInstructionFileName(name: string): name is InstructionFileName {\n return (INSTRUCTION_FILE_NAMES as readonly string[]).includes(name);\n}\n\n/** One instruction file on disk, read. */\nexport interface InstructionFile {\n /** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */\n readonly path: string;\n readonly name: InstructionFileName;\n /** The directory it sits in, root-relative; `''` for the repository root. */\n readonly dir: string;\n readonly text: string;\n /** UTF-8 bytes. The unit every smelt budget is in. */\n readonly bytes: number;\n /**\n * True when the entry is a symlink — the arrangement the guide recommends for\n * mirrors, and the one arrangement in which a mirror **cannot** drift.\n */\n readonly symlink: boolean;\n}\n\n/**\n * One directory's contribution: the file that level costs, plus the mirrors of it.\n *\n * `primary` is the only one whose bytes are summed. A mirror that differs from it is\n * `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason\n * the guide suggests one.\n */\nexport interface InstructionLevel {\n /** Root-relative directory; `''` is the root level. */\n readonly dir: string;\n readonly primary: InstructionFile;\n readonly mirrors: readonly InstructionFile[];\n}\n\n/** Every instruction file in a tree, arranged by level. */\nexport interface InstructionSet {\n /** The directory the walk started from, as the caller spelled it. */\n readonly root: string;\n /** Root level first, then nested levels in walk order. */\n readonly levels: readonly InstructionLevel[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost.** Sibling levels never merge, so no single request ever\n * loads all of these — see {@link perRequestBytes} for the number that answers that\n * question.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus every level\n * above it, since a nested file merges with its **ancestors** and with nothing else.\n *\n * Equal to {@link totalBytes} in a repository with one chain of levels, and strictly\n * smaller as soon as two siblings both carry an instruction file.\n */\n readonly perRequestBytes: number;\n}\n\n/** How deep the walk goes. Instruction files near the root are the ones agents read. */\nconst MAX_DEPTH = 6;\n\n/**\n * Find and read every instruction file under `root`.\n *\n * The ignore list is the repo map's, and for the same reason: a vendored\n * `node_modules/**\\/AGENTS.md` is not this repository's instruction to an agent, and\n * counting it would make the total a number about somebody else's project. A caller\n * may replace the list wholesale — a default nobody can turn off is not a default.\n *\n * Symlinked *directories* are refused outright, exactly as the map refuses them: a\n * walk that follows one can leave the root. A symlinked instruction **file** is read\n * (following it is the point — that is what the harness does too) and flagged.\n */\nexport function readInstructionSet(options: {\n readonly root: string;\n readonly reader?: RepoReader;\n readonly ignore?: readonly string[];\n}): InstructionSet {\n const reader = options.reader ?? nodeFsReader();\n const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;\n const found: InstructionFile[] = [];\n\n const walk = (dir: string, depth: number): void => {\n const entries = reader\n .list(dir === '' ? options.root : join(options.root, dir))\n .map((entry) => entry.name)\n .toSorted();\n\n for (const name of entries) {\n const relative = dir === '' ? name : `${dir}/${name}`;\n if (isIgnored(relative, ignore)) continue;\n const stat = reader.stat(join(options.root, relative));\n if (stat === undefined) continue;\n if (stat.isDirectory && !stat.isSymlink) {\n if (depth < MAX_DEPTH) walk(relative, depth + 1);\n continue;\n }\n if (!isInstructionFileName(name)) continue;\n // A symlinked instruction file is **read**, unlike a symlinked directory, which\n // is refused. Following it is the point: it is the arrangement the guide\n // recommends for mirrors, and it is exactly what the harness does when it opens\n // `CLAUDE.md`. Two readers disagree about how to describe one, and both are\n // accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*\n // `isFile`; a resolving reader answers about the target, so it is both.\n if (!stat.isFile && !stat.isSymlink) continue;\n let raw: Uint8Array;\n try {\n raw = reader.read(join(options.root, relative));\n } catch {\n // A broken symlink, or a file that vanished between the stat and the read.\n // Nothing is loaded on every request, so there is nothing to report.\n continue;\n }\n found.push({\n path: relative,\n name,\n dir,\n text: new TextDecoder('utf-8').decode(raw),\n // The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.\n // Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the\n // way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes\n // back as a three-byte U+FFFD. Both skew the one number this verb exists to\n // state, in opposite directions, against a figure the harness measures on the\n // file itself. `stat.size` is not the answer either: on a symlinked mirror\n // `lstat` sizes the *link*, and following it is the point.\n bytes: raw.byteLength,\n symlink: stat.isSymlink,\n });\n }\n };\n\n walk('', 0);\n\n const byDir = new Map<string, InstructionFile[]>();\n for (const file of found) {\n const bucket = byDir.get(file.dir);\n if (bucket === undefined) byDir.set(file.dir, [file]);\n else bucket.push(file);\n }\n\n const levels: InstructionLevel[] = [...byDir.entries()]\n // Root first, then by depth and then alphabetically: the order a reader would\n // walk the tree in, so a report reads top-down.\n .toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))\n .map(([dir, files]) => {\n const ordered = files.toSorted(\n (a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name),\n );\n return { dir, primary: ordered[0]!, mirrors: ordered.slice(1) };\n });\n\n return {\n root: options.root,\n levels,\n totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),\n perRequestBytes: heaviestChainBytes(levels),\n };\n}\n\n/**\n * The heaviest ancestor chain: the most any single request can load.\n *\n * A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is\n * a sibling, and no agent loads a sibling — so the maximum over chains is the honest\n * per-request figure, and the sum over levels is not one.\n */\nfunction heaviestChainBytes(levels: readonly InstructionLevel[]): number {\n const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));\n let heaviest = 0;\n for (const level of levels) {\n const chain = ancestorDirs(level.dir).reduce(\n (sum, dir) => sum + (bytesByDir.get(dir) ?? 0),\n level.primary.bytes,\n );\n heaviest = Math.max(heaviest, chain);\n }\n return heaviest;\n}\n\n/**\n * The directories a level merges *with*, root first and nearest last — and never the\n * level itself.\n *\n * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}\n * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.\n */\nexport function ancestorDirs(dir: string): readonly string[] {\n if (dir === '') return [];\n const segments = dir.split('/');\n const out: string[] = [''];\n for (let cut = 1; cut < segments.length; cut += 1) out.push(segments.slice(0, cut).join('/'));\n return out;\n}\n\n/** `''` is depth 0; `a/b` is depth 2. */\nfunction depthOf(dir: string): number {\n return dir === '' ? 0 : dir.split('/').length;\n}\n\n/**\n * The repo map's ignore semantics, restated over root-relative paths: a bare name\n * matches any path segment, an entry containing `/` is a root-relative prefix.\n */\nfunction isIgnored(relative: string, ignore: readonly string[]): boolean {\n const segments = relative.split('/');\n return ignore.some((entry) => {\n const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;\n if (trimmed === '') return false;\n if (entry.includes('/')) {\n return relative === trimmed || relative.startsWith(`${trimmed}/`);\n }\n return segments.includes(trimmed);\n });\n}\n\n/**\n * Does `token`, read as a path relative to the repository root, name something that\n * exists? The one question `dead-path` and `dead-link` both ask.\n *\n * Through the reader, so the resolution is part of the same injectable walk the\n * discovery is — a fixture repo can therefore contain a path that resolves and one\n * that does not without either of them existing on the machine running the tests.\n * A reader that throws (a malformed path, a permission error) answers \"no\": the\n * finding is advisory, and a lint that failed the whole run because one token in\n * somebody's prose was unstattable would be worse than the staleness it looks for.\n */\nexport function resolvesInTree(root: string, reader: RepoReader, token: string): boolean {\n try {\n return reader.stat(join(root, token)) !== undefined;\n } catch {\n return false;\n }\n}\n"]}
@@ -0,0 +1,212 @@
1
+ import { join } from 'node:path';
2
+ import { DEFAULT_REPO_IGNORE } from '../repomap/map.js';
3
+ import { nodeFsReader } from '../repomap/reader.js';
4
+ /**
5
+ * The instruction files an agent actually loads, found through the read-only
6
+ * {@link RepoReader} seam.
7
+ *
8
+ * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule
9
+ * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so
10
+ * a lint that reported only the root file would be reporting a fraction and calling it
11
+ * the total, which is this repository's Law 4 pointed at its own output.
12
+ *
13
+ * **But a merge runs up the tree, not across it.** Two numbers come out of that, and
14
+ * they are different numbers in any monorepo, so both are reported and each is labelled
15
+ * with the question it answers:
16
+ *
17
+ * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level
18
+ * plus its **ancestors**. An agent working in `pkg/a` loads the root file and
19
+ * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the
20
+ * per-request cost, and it is the number the guide's argument is about.
21
+ * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole
22
+ * repository's instruction surface. Useful as the size of what a team maintains,
23
+ * and **not** a per-request cost. Calling it one would be the same over-count this
24
+ * module refuses for mirrors one paragraph down.
25
+ *
26
+ * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads
27
+ * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so
28
+ * summing all three would triple a cost nobody pays. At each directory the primary is
29
+ * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever
30
+ * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted
31
+ * for drift and not for bytes.
32
+ *
33
+ * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:
34
+ * that a nested file was found, that an ignored directory was never entered, that a
35
+ * path in the prose resolves against the real tree. Those are claims about calls, so
36
+ * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),
37
+ * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing
38
+ * changes for a caller who does not care.
39
+ */
40
+ /** The three file names a coding agent reads instructions from, in precedence order. */
41
+ export const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'];
42
+ /** True for a name this module treats as an instruction file. */
43
+ export function isInstructionFileName(name) {
44
+ return INSTRUCTION_FILE_NAMES.includes(name);
45
+ }
46
+ /** How deep the walk goes. Instruction files near the root are the ones agents read. */
47
+ const MAX_DEPTH = 6;
48
+ /**
49
+ * Find and read every instruction file under `root`.
50
+ *
51
+ * The ignore list is the repo map's, and for the same reason: a vendored
52
+ * `node_modules/**\/AGENTS.md` is not this repository's instruction to an agent, and
53
+ * counting it would make the total a number about somebody else's project. A caller
54
+ * may replace the list wholesale — a default nobody can turn off is not a default.
55
+ *
56
+ * Symlinked *directories* are refused outright, exactly as the map refuses them: a
57
+ * walk that follows one can leave the root. A symlinked instruction **file** is read
58
+ * (following it is the point — that is what the harness does too) and flagged.
59
+ */
60
+ export function readInstructionSet(options) {
61
+ const reader = options.reader ?? nodeFsReader();
62
+ const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;
63
+ const found = [];
64
+ const walk = (dir, depth) => {
65
+ const entries = reader
66
+ .list(dir === '' ? options.root : join(options.root, dir))
67
+ .map((entry) => entry.name)
68
+ .toSorted();
69
+ for (const name of entries) {
70
+ const relative = dir === '' ? name : `${dir}/${name}`;
71
+ if (isIgnored(relative, ignore))
72
+ continue;
73
+ const stat = reader.stat(join(options.root, relative));
74
+ if (stat === undefined)
75
+ continue;
76
+ if (stat.isDirectory && !stat.isSymlink) {
77
+ if (depth < MAX_DEPTH)
78
+ walk(relative, depth + 1);
79
+ continue;
80
+ }
81
+ if (!isInstructionFileName(name))
82
+ continue;
83
+ // A symlinked instruction file is **read**, unlike a symlinked directory, which
84
+ // is refused. Following it is the point: it is the arrangement the guide
85
+ // recommends for mirrors, and it is exactly what the harness does when it opens
86
+ // `CLAUDE.md`. Two readers disagree about how to describe one, and both are
87
+ // accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*
88
+ // `isFile`; a resolving reader answers about the target, so it is both.
89
+ if (!stat.isFile && !stat.isSymlink)
90
+ continue;
91
+ let raw;
92
+ try {
93
+ raw = reader.read(join(options.root, relative));
94
+ }
95
+ catch {
96
+ // A broken symlink, or a file that vanished between the stat and the read.
97
+ // Nothing is loaded on every request, so there is nothing to report.
98
+ continue;
99
+ }
100
+ found.push({
101
+ path: relative,
102
+ name,
103
+ dir,
104
+ text: new TextDecoder('utf-8').decode(raw),
105
+ // The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.
106
+ // Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the
107
+ // way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes
108
+ // back as a three-byte U+FFFD. Both skew the one number this verb exists to
109
+ // state, in opposite directions, against a figure the harness measures on the
110
+ // file itself. `stat.size` is not the answer either: on a symlinked mirror
111
+ // `lstat` sizes the *link*, and following it is the point.
112
+ bytes: raw.byteLength,
113
+ symlink: stat.isSymlink,
114
+ });
115
+ }
116
+ };
117
+ walk('', 0);
118
+ const byDir = new Map();
119
+ for (const file of found) {
120
+ const bucket = byDir.get(file.dir);
121
+ if (bucket === undefined)
122
+ byDir.set(file.dir, [file]);
123
+ else
124
+ bucket.push(file);
125
+ }
126
+ const levels = [...byDir.entries()]
127
+ // Root first, then by depth and then alphabetically: the order a reader would
128
+ // walk the tree in, so a report reads top-down.
129
+ .toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))
130
+ .map(([dir, files]) => {
131
+ const ordered = files.toSorted((a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name));
132
+ return { dir, primary: ordered[0], mirrors: ordered.slice(1) };
133
+ });
134
+ return {
135
+ root: options.root,
136
+ levels,
137
+ totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),
138
+ perRequestBytes: heaviestChainBytes(levels),
139
+ };
140
+ }
141
+ /**
142
+ * The heaviest ancestor chain: the most any single request can load.
143
+ *
144
+ * A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is
145
+ * a sibling, and no agent loads a sibling — so the maximum over chains is the honest
146
+ * per-request figure, and the sum over levels is not one.
147
+ */
148
+ function heaviestChainBytes(levels) {
149
+ const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));
150
+ let heaviest = 0;
151
+ for (const level of levels) {
152
+ const chain = ancestorDirs(level.dir).reduce((sum, dir) => sum + (bytesByDir.get(dir) ?? 0), level.primary.bytes);
153
+ heaviest = Math.max(heaviest, chain);
154
+ }
155
+ return heaviest;
156
+ }
157
+ /**
158
+ * The directories a level merges *with*, root first and nearest last — and never the
159
+ * level itself.
160
+ *
161
+ * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}
162
+ * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.
163
+ */
164
+ export function ancestorDirs(dir) {
165
+ if (dir === '')
166
+ return [];
167
+ const segments = dir.split('/');
168
+ const out = [''];
169
+ for (let cut = 1; cut < segments.length; cut += 1)
170
+ out.push(segments.slice(0, cut).join('/'));
171
+ return out;
172
+ }
173
+ /** `''` is depth 0; `a/b` is depth 2. */
174
+ function depthOf(dir) {
175
+ return dir === '' ? 0 : dir.split('/').length;
176
+ }
177
+ /**
178
+ * The repo map's ignore semantics, restated over root-relative paths: a bare name
179
+ * matches any path segment, an entry containing `/` is a root-relative prefix.
180
+ */
181
+ function isIgnored(relative, ignore) {
182
+ const segments = relative.split('/');
183
+ return ignore.some((entry) => {
184
+ const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;
185
+ if (trimmed === '')
186
+ return false;
187
+ if (entry.includes('/')) {
188
+ return relative === trimmed || relative.startsWith(`${trimmed}/`);
189
+ }
190
+ return segments.includes(trimmed);
191
+ });
192
+ }
193
+ /**
194
+ * Does `token`, read as a path relative to the repository root, name something that
195
+ * exists? The one question `dead-path` and `dead-link` both ask.
196
+ *
197
+ * Through the reader, so the resolution is part of the same injectable walk the
198
+ * discovery is — a fixture repo can therefore contain a path that resolves and one
199
+ * that does not without either of them existing on the machine running the tests.
200
+ * A reader that throws (a malformed path, a permission error) answers "no": the
201
+ * finding is advisory, and a lint that failed the whole run because one token in
202
+ * somebody's prose was unstattable would be worse than the staleness it looks for.
203
+ */
204
+ export function resolvesInTree(root, reader, token) {
205
+ try {
206
+ return reader.stat(join(root, token)) !== undefined;
207
+ }
208
+ catch {
209
+ return false;
210
+ }
211
+ }
212
+ //# sourceMappingURL=instructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.js","sourceRoot":"","sources":["../../src/agents/instructions.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from 'node:path';\n\nimport { DEFAULT_REPO_IGNORE } from '../repomap/map.ts';\nimport { nodeFsReader } from '../repomap/reader.ts';\nimport type { RepoReader } from '../repomap/reader.ts';\n\n/**\n * The instruction files an agent actually loads, found through the read-only\n * {@link RepoReader} seam.\n *\n * **Why the merged set, and not just the root file** (ruling R8): the guide's own rule\n * is that a nested `AGENTS.md` *merges with* the root one rather than replacing it, so\n * a lint that reported only the root file would be reporting a fraction and calling it\n * the total, which is this repository's Law 4 pointed at its own output.\n *\n * **But a merge runs up the tree, not across it.** Two numbers come out of that, and\n * they are different numbers in any monorepo, so both are reported and each is labelled\n * with the question it answers:\n *\n * - {@link InstructionSet.perRequestBytes} — the most any *one* request loads: a level\n * plus its **ancestors**. An agent working in `pkg/a` loads the root file and\n * `pkg/a`'s; it never loads `pkg/b`'s, because siblings do not merge. This is the\n * per-request cost, and it is the number the guide's argument is about.\n * - {@link InstructionSet.totalBytes} — every level's primary summed: the whole\n * repository's instruction surface. Useful as the size of what a team maintains,\n * and **not** a per-request cost. Calling it one would be the same over-count this\n * module refuses for mirrors one paragraph down.\n *\n * **Why a mirror is not a level.** Claude Code reads `CLAUDE.md`, Gemini reads\n * `GEMINI.md`, Codex and the rest read `AGENTS.md`. One agent loads *one* of them, so\n * summing all three would triple a cost nobody pays. At each directory the primary is\n * the file the level contributes — `AGENTS.md` when it exists, otherwise whichever\n * mirror is there alone — and the others are {@link InstructionLevel.mirrors}, counted\n * for drift and not for bytes.\n *\n * **Why the reader is a seam.** Every claim the lint makes is a claim about a *walk*:\n * that a nested file was found, that an ignored directory was never entered, that a\n * path in the prose resolves against the real tree. Those are claims about calls, so\n * they are asserted by counting calls against a stub tree (`test/repo-reader-stub.ts`),\n * exactly as the repo map's walk is. The default is {@link nodeFsReader}, so nothing\n * changes for a caller who does not care.\n */\n\n/** The three file names a coding agent reads instructions from, in precedence order. */\nexport const INSTRUCTION_FILE_NAMES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md'] as const;\n\n/** One of the three names above. */\nexport type InstructionFileName = (typeof INSTRUCTION_FILE_NAMES)[number];\n\n/** True for a name this module treats as an instruction file. */\nexport function isInstructionFileName(name: string): name is InstructionFileName {\n return (INSTRUCTION_FILE_NAMES as readonly string[]).includes(name);\n}\n\n/** One instruction file on disk, read. */\nexport interface InstructionFile {\n /** Root-relative and `/`-separated, e.g. `packages/core/AGENTS.md`. */\n readonly path: string;\n readonly name: InstructionFileName;\n /** The directory it sits in, root-relative; `''` for the repository root. */\n readonly dir: string;\n readonly text: string;\n /** UTF-8 bytes. The unit every smelt budget is in. */\n readonly bytes: number;\n /**\n * True when the entry is a symlink — the arrangement the guide recommends for\n * mirrors, and the one arrangement in which a mirror **cannot** drift.\n */\n readonly symlink: boolean;\n}\n\n/**\n * One directory's contribution: the file that level costs, plus the mirrors of it.\n *\n * `primary` is the only one whose bytes are summed. A mirror that differs from it is\n * `mirror-drift`; a mirror that is a symlink cannot differ, which is the whole reason\n * the guide suggests one.\n */\nexport interface InstructionLevel {\n /** Root-relative directory; `''` is the root level. */\n readonly dir: string;\n readonly primary: InstructionFile;\n readonly mirrors: readonly InstructionFile[];\n}\n\n/** Every instruction file in a tree, arranged by level. */\nexport interface InstructionSet {\n /** The directory the walk started from, as the caller spelled it. */\n readonly root: string;\n /** Root level first, then nested levels in walk order. */\n readonly levels: readonly InstructionLevel[];\n /**\n * The repository-wide instruction surface: every level's primary, summed.\n *\n * **Not a per-request cost.** Sibling levels never merge, so no single request ever\n * loads all of these — see {@link perRequestBytes} for the number that answers that\n * question.\n */\n readonly totalBytes: number;\n /**\n * What the most expensive single request loads: the heaviest level plus every level\n * above it, since a nested file merges with its **ancestors** and with nothing else.\n *\n * Equal to {@link totalBytes} in a repository with one chain of levels, and strictly\n * smaller as soon as two siblings both carry an instruction file.\n */\n readonly perRequestBytes: number;\n}\n\n/** How deep the walk goes. Instruction files near the root are the ones agents read. */\nconst MAX_DEPTH = 6;\n\n/**\n * Find and read every instruction file under `root`.\n *\n * The ignore list is the repo map's, and for the same reason: a vendored\n * `node_modules/**\\/AGENTS.md` is not this repository's instruction to an agent, and\n * counting it would make the total a number about somebody else's project. A caller\n * may replace the list wholesale — a default nobody can turn off is not a default.\n *\n * Symlinked *directories* are refused outright, exactly as the map refuses them: a\n * walk that follows one can leave the root. A symlinked instruction **file** is read\n * (following it is the point — that is what the harness does too) and flagged.\n */\nexport function readInstructionSet(options: {\n readonly root: string;\n readonly reader?: RepoReader;\n readonly ignore?: readonly string[];\n}): InstructionSet {\n const reader = options.reader ?? nodeFsReader();\n const ignore = options.ignore ?? DEFAULT_REPO_IGNORE;\n const found: InstructionFile[] = [];\n\n const walk = (dir: string, depth: number): void => {\n const entries = reader\n .list(dir === '' ? options.root : join(options.root, dir))\n .map((entry) => entry.name)\n .toSorted();\n\n for (const name of entries) {\n const relative = dir === '' ? name : `${dir}/${name}`;\n if (isIgnored(relative, ignore)) continue;\n const stat = reader.stat(join(options.root, relative));\n if (stat === undefined) continue;\n if (stat.isDirectory && !stat.isSymlink) {\n if (depth < MAX_DEPTH) walk(relative, depth + 1);\n continue;\n }\n if (!isInstructionFileName(name)) continue;\n // A symlinked instruction file is **read**, unlike a symlinked directory, which\n // is refused. Following it is the point: it is the arrangement the guide\n // recommends for mirrors, and it is exactly what the harness does when it opens\n // `CLAUDE.md`. Two readers disagree about how to describe one, and both are\n // accommodated here: `nodeFsReader` lstats, so a link is `isSymlink` and *not*\n // `isFile`; a resolving reader answers about the target, so it is both.\n if (!stat.isFile && !stat.isSymlink) continue;\n let raw: Uint8Array;\n try {\n raw = reader.read(join(options.root, relative));\n } catch {\n // A broken symlink, or a file that vanished between the stat and the read.\n // Nothing is loaded on every request, so there is nothing to report.\n continue;\n }\n found.push({\n path: relative,\n name,\n dir,\n text: new TextDecoder('utf-8').decode(raw),\n // The bytes on disk, straight off the read — never `Buffer.byteLength(text)`.\n // Decoding and re-encoding is not the identity: a UTF-8 BOM is dropped on the\n // way in (19 bytes read, 16 reported) and a byte that is not valid UTF-8 comes\n // back as a three-byte U+FFFD. Both skew the one number this verb exists to\n // state, in opposite directions, against a figure the harness measures on the\n // file itself. `stat.size` is not the answer either: on a symlinked mirror\n // `lstat` sizes the *link*, and following it is the point.\n bytes: raw.byteLength,\n symlink: stat.isSymlink,\n });\n }\n };\n\n walk('', 0);\n\n const byDir = new Map<string, InstructionFile[]>();\n for (const file of found) {\n const bucket = byDir.get(file.dir);\n if (bucket === undefined) byDir.set(file.dir, [file]);\n else bucket.push(file);\n }\n\n const levels: InstructionLevel[] = [...byDir.entries()]\n // Root first, then by depth and then alphabetically: the order a reader would\n // walk the tree in, so a report reads top-down.\n .toSorted(([a], [b]) => depthOf(a) - depthOf(b) || (a < b ? -1 : a > b ? 1 : 0))\n .map(([dir, files]) => {\n const ordered = files.toSorted(\n (a, b) => INSTRUCTION_FILE_NAMES.indexOf(a.name) - INSTRUCTION_FILE_NAMES.indexOf(b.name),\n );\n return { dir, primary: ordered[0]!, mirrors: ordered.slice(1) };\n });\n\n return {\n root: options.root,\n levels,\n totalBytes: levels.reduce((sum, level) => sum + level.primary.bytes, 0),\n perRequestBytes: heaviestChainBytes(levels),\n };\n}\n\n/**\n * The heaviest ancestor chain: the most any single request can load.\n *\n * A request made in `pkg/a` merges the root level with `pkg/a`'s, and stops. `pkg/b` is\n * a sibling, and no agent loads a sibling — so the maximum over chains is the honest\n * per-request figure, and the sum over levels is not one.\n */\nfunction heaviestChainBytes(levels: readonly InstructionLevel[]): number {\n const bytesByDir = new Map(levels.map((level) => [level.dir, level.primary.bytes]));\n let heaviest = 0;\n for (const level of levels) {\n const chain = ancestorDirs(level.dir).reduce(\n (sum, dir) => sum + (bytesByDir.get(dir) ?? 0),\n level.primary.bytes,\n );\n heaviest = Math.max(heaviest, chain);\n }\n return heaviest;\n}\n\n/**\n * The directories a level merges *with*, root first and nearest last — and never the\n * level itself.\n *\n * The whole distinction `restated-at-level` and {@link InstructionSet.perRequestBytes}\n * both turn on: `pkg/a` merges with `''` and `pkg`, and with nothing else in the tree.\n */\nexport function ancestorDirs(dir: string): readonly string[] {\n if (dir === '') return [];\n const segments = dir.split('/');\n const out: string[] = [''];\n for (let cut = 1; cut < segments.length; cut += 1) out.push(segments.slice(0, cut).join('/'));\n return out;\n}\n\n/** `''` is depth 0; `a/b` is depth 2. */\nfunction depthOf(dir: string): number {\n return dir === '' ? 0 : dir.split('/').length;\n}\n\n/**\n * The repo map's ignore semantics, restated over root-relative paths: a bare name\n * matches any path segment, an entry containing `/` is a root-relative prefix.\n */\nfunction isIgnored(relative: string, ignore: readonly string[]): boolean {\n const segments = relative.split('/');\n return ignore.some((entry) => {\n const trimmed = entry.endsWith('/') ? entry.slice(0, -1) : entry;\n if (trimmed === '') return false;\n if (entry.includes('/')) {\n return relative === trimmed || relative.startsWith(`${trimmed}/`);\n }\n return segments.includes(trimmed);\n });\n}\n\n/**\n * Does `token`, read as a path relative to the repository root, name something that\n * exists? The one question `dead-path` and `dead-link` both ask.\n *\n * Through the reader, so the resolution is part of the same injectable walk the\n * discovery is — a fixture repo can therefore contain a path that resolves and one\n * that does not without either of them existing on the machine running the tests.\n * A reader that throws (a malformed path, a permission error) answers \"no\": the\n * finding is advisory, and a lint that failed the whole run because one token in\n * somebody's prose was unstattable would be worse than the staleness it looks for.\n */\nexport function resolvesInTree(root: string, reader: RepoReader, token: string): boolean {\n try {\n return reader.stat(join(root, token)) !== undefined;\n } catch {\n return false;\n }\n}\n"]}