@bevel-software/platform-core-backend 0.25.0 → 0.26.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 (254) hide show
  1. package/agent-guide/access-control.md +234 -0
  2. package/agent-guide/conventions.md +27 -0
  3. package/agent-guide/directory-structure.md +145 -0
  4. package/agent-guide/finding-things.md +7 -0
  5. package/agent-guide/introduction.md +27 -0
  6. package/agent-guide/skills.md +47 -0
  7. package/agent-guide/tool-manuals.md +217 -0
  8. package/agent-guide/where-a-new-file-goes.md +36 -0
  9. package/dist/assets.d.ts +7 -0
  10. package/dist/assets.d.ts.map +1 -1
  11. package/dist/assets.js +9 -0
  12. package/dist/assets.js.map +1 -1
  13. package/dist/core/core-ports.d.ts +11 -0
  14. package/dist/core/core-ports.d.ts.map +1 -1
  15. package/dist/core/core-ports.js.map +1 -1
  16. package/dist/core/create-core-server.d.ts.map +1 -1
  17. package/dist/core/create-core-server.js +13 -2
  18. package/dist/core/create-core-server.js.map +1 -1
  19. package/dist/core/create-core-services.d.ts +9 -0
  20. package/dist/core/create-core-services.d.ts.map +1 -1
  21. package/dist/core/create-core-services.js +14 -4
  22. package/dist/core/create-core-services.js.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/modules/access/access-control.interface.d.ts +9 -0
  28. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  29. package/dist/modules/access/access-control.service.d.ts +1 -0
  30. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  31. package/dist/modules/access/access-control.service.js +16 -0
  32. package/dist/modules/access/access-control.service.js.map +1 -1
  33. package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
  34. package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
  35. package/dist/modules/agent-guide/agent-guide.js +191 -0
  36. package/dist/modules/agent-guide/agent-guide.js.map +1 -0
  37. package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
  38. package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
  39. package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
  40. package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
  41. package/dist/modules/agent-guide/index.d.ts +4 -0
  42. package/dist/modules/agent-guide/index.d.ts.map +1 -0
  43. package/dist/modules/agent-guide/index.js +4 -0
  44. package/dist/modules/agent-guide/index.js.map +1 -0
  45. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
  46. package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
  47. package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
  48. package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
  49. package/dist/modules/agent-instructions/compose.d.ts +9 -6
  50. package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
  51. package/dist/modules/agent-instructions/compose.js +9 -6
  52. package/dist/modules/agent-instructions/compose.js.map +1 -1
  53. package/dist/modules/agent-instructions/index.d.ts +1 -1
  54. package/dist/modules/agent-instructions/index.d.ts.map +1 -1
  55. package/dist/modules/agent-instructions/index.js +1 -1
  56. package/dist/modules/agent-instructions/index.js.map +1 -1
  57. package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
  58. package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
  59. package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
  60. package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
  61. package/dist/modules/mcp/mcp.service.d.ts +29 -2
  62. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  63. package/dist/modules/mcp/mcp.service.js +113 -16
  64. package/dist/modules/mcp/mcp.service.js.map +1 -1
  65. package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
  66. package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
  67. package/dist/modules/mcp/tool-schema-guard.js +171 -0
  68. package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
  69. package/dist/modules/plugins/plugins.tools.d.ts +36 -2
  70. package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
  71. package/dist/modules/plugins/plugins.tools.js +71 -14
  72. package/dist/modules/plugins/plugins.tools.js.map +1 -1
  73. package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
  74. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  75. package/dist/modules/settings/deployment-settings.service.js +14 -53
  76. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  77. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  78. package/dist/modules/settings/setup.routes.js +3 -6
  79. package/dist/modules/settings/setup.routes.js.map +1 -1
  80. package/dist/modules/skills/skills.tools.d.ts.map +1 -1
  81. package/dist/modules/skills/skills.tools.js +58 -16
  82. package/dist/modules/skills/skills.tools.js.map +1 -1
  83. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
  84. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  85. package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
  86. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
  87. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  88. package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
  89. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  90. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
  91. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
  93. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  94. package/dist/modules/tool-registry/description-length.d.ts +14 -14
  95. package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
  96. package/dist/modules/tool-registry/description-length.js +24 -26
  97. package/dist/modules/tool-registry/description-length.js.map +1 -1
  98. package/dist/modules/tool-registry/guide-first.d.ts +23 -0
  99. package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
  100. package/dist/modules/tool-registry/guide-first.js +32 -0
  101. package/dist/modules/tool-registry/guide-first.js.map +1 -0
  102. package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
  103. package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
  104. package/dist/modules/tool-registry/tool-registry.js +9 -2
  105. package/dist/modules/tool-registry/tool-registry.js.map +1 -1
  106. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
  107. package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
  108. package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
  109. package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
  110. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
  111. package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
  112. package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
  113. package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
  114. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
  115. package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
  116. package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
  117. package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
  118. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  119. package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
  120. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  121. package/dist/modules/workflow/git/git.service.d.ts +210 -13
  122. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  123. package/dist/modules/workflow/git/git.service.js +456 -91
  124. package/dist/modules/workflow/git/git.service.js.map +1 -1
  125. package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
  126. package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
  127. package/dist/modules/workflow/git/merge-commit.js +89 -0
  128. package/dist/modules/workflow/git/merge-commit.js.map +1 -0
  129. package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
  130. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  131. package/dist/modules/workflow/git/pull-request.service.js +332 -37
  132. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  133. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
  134. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  135. package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
  136. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  137. package/dist/modules/workflow/workflow.routes.d.ts +6 -2
  138. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  139. package/dist/modules/workflow/workflow.routes.js +7 -2
  140. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  141. package/dist/modules/workflow/workflow.service.d.ts +4 -0
  142. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  143. package/dist/modules/workflow/workflow.service.js +3 -0
  144. package/dist/modules/workflow/workflow.service.js.map +1 -1
  145. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +70 -0
  146. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
  147. package/dist/modules/workspace/startup/kb-startup-runner.js +213 -20
  148. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  149. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
  150. package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
  151. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  152. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
  153. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
  154. package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
  155. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
  156. package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
  157. package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
  158. package/dist/modules/workspace/startup/steps/template-source.js +5 -3
  159. package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
  160. package/dist/modules/workspace/workspace.tools.d.ts +10 -1
  161. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  162. package/dist/modules/workspace/workspace.tools.js +211 -18
  163. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  164. package/dist/shared/domain-errors.d.ts +11 -0
  165. package/dist/shared/domain-errors.d.ts.map +1 -1
  166. package/dist/shared/domain-errors.js +14 -0
  167. package/dist/shared/domain-errors.js.map +1 -1
  168. package/dist/shared/hidden-tools.d.ts +44 -0
  169. package/dist/shared/hidden-tools.d.ts.map +1 -0
  170. package/dist/shared/hidden-tools.js +13 -0
  171. package/dist/shared/hidden-tools.js.map +1 -0
  172. package/kb-template/.bevelignore +0 -5
  173. package/package.json +4 -3
  174. package/src/__tests__/kb-layout-config.test.ts +10 -100
  175. package/src/__tests__/packaged-assets-ship.test.ts +54 -0
  176. package/src/assets.ts +10 -0
  177. package/src/core/core-ports.ts +11 -0
  178. package/src/core/create-core-server.ts +13 -2
  179. package/src/core/create-core-services.ts +28 -4
  180. package/src/index.ts +2 -2
  181. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
  182. package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
  183. package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
  184. package/src/modules/access/access-control.interface.ts +15 -0
  185. package/src/modules/access/access-control.service.ts +21 -0
  186. package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
  187. package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
  188. package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
  189. package/src/modules/agent-guide/agent-guide.ts +291 -0
  190. package/src/modules/agent-guide/index.ts +21 -0
  191. package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
  192. package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
  193. package/src/modules/agent-instructions/compose.ts +9 -6
  194. package/src/modules/agent-instructions/index.ts +0 -3
  195. package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
  196. package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
  197. package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
  198. package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
  199. package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
  200. package/src/modules/mcp/mcp.service.ts +137 -19
  201. package/src/modules/mcp/tool-schema-guard.ts +196 -0
  202. package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
  203. package/src/modules/plugins/plugins.tools.ts +75 -15
  204. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
  205. package/src/modules/settings/deployment-settings.service.ts +13 -54
  206. package/src/modules/settings/setup.routes.ts +3 -6
  207. package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
  208. package/src/modules/skills/skills.tools.ts +62 -16
  209. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
  210. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
  211. package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
  212. package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
  213. package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
  214. package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
  215. package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
  216. package/src/modules/tool-registry/description-length.ts +24 -26
  217. package/src/modules/tool-registry/guide-first.ts +34 -0
  218. package/src/modules/tool-registry/tool-registry.ts +9 -2
  219. package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
  220. package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
  221. package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
  222. package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
  223. package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
  224. package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
  225. package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
  226. package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
  227. package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
  228. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
  229. package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
  230. package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
  231. package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
  232. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
  233. package/src/modules/workflow/git/git.service.ts +537 -94
  234. package/src/modules/workflow/git/merge-commit.ts +88 -0
  235. package/src/modules/workflow/git/pull-request.service.ts +380 -54
  236. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
  237. package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
  238. package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
  239. package/src/modules/workflow/workflow.routes.ts +7 -2
  240. package/src/modules/workflow/workflow.service.ts +7 -0
  241. package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
  242. package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
  243. package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
  244. package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
  245. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +231 -1
  246. package/src/modules/workspace/startup/kb-startup-runner.ts +216 -19
  247. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
  248. package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
  249. package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
  250. package/src/modules/workspace/startup/steps/template-source.ts +5 -3
  251. package/src/modules/workspace/workspace.tools.ts +226 -16
  252. package/src/shared/domain-errors.ts +15 -0
  253. package/src/shared/hidden-tools.ts +45 -0
  254. package/kb-template/AGENTS.md +0 -730
@@ -14,11 +14,10 @@ import { PluginManifestsStep } from '../plugin-manifests.step.js';
14
14
  import { PersonalSpacesStep } from '../personal-spaces.step.js';
15
15
  import { RolesYamlStep } from '../roles-yaml.step.js';
16
16
  import { renderRolesYaml } from '../../../../access-model/render-roles-yaml.js';
17
- import { mergeGroupsIntoRoles, parseRolesYaml } from '../../../../access-model/access-grammar.js';
18
17
  import { TemplateFilesStep } from '../template-files.step.js';
19
18
  import { buildSeedTree } from '../seed-tree.js';
20
19
  import { defaultKbTemplateDir } from '../../../../../assets.js';
21
- import { DEFAULT_KB_LAYOUT, agentsFilePointerSentence } from '@bevel-software/platform-shared';
20
+ import { DEFAULT_KB_LAYOUT } from '@bevel-software/platform-shared';
22
21
  import { renderTemplateText } from '../template-source.js';
23
22
  import { TOOL_PREFIX_LINE, composeAgentInstructions, platformInstructions } from '../../../../agent-instructions/index.js';
24
23
  import { testKbContext } from '../../../../../__tests__/kb-context.js';
@@ -139,6 +138,11 @@ async function readJson(dir: string, rel: string): Promise<Record<string, any>>
139
138
  }
140
139
 
141
140
  const norm = (text: string) => text.replace(/\r\n?/g, '\n');
141
+
142
+ /** What a release that wrote the guide to disk left at the root: the header that proves the copy ours. */
143
+ const MANAGED_GUIDE_COPY =
144
+ '# Knowledge base\n\n> **This file is managed by the platform.** It lives at the repository root as\n' +
145
+ '> `AGENTS.md`, and every server restart replaces it with the current template.\n\n## Directory Structure\n';
142
146
  const PREAMBLE_IGNORE_BLOCK =
143
147
  '\n# Added by the platform: agent instructions are edited from External agent access.\n' +
144
148
  '/mcp-description.md\n';
@@ -155,7 +159,6 @@ async function template(name: string): Promise<string> {
155
159
  async function fullScaffold(): Promise<Record<string, string>> {
156
160
  return {
157
161
  'access.md': await template('access.md'),
158
- 'AGENTS.md': await template('AGENTS.md'),
159
162
  '.bevelignore': await template('.bevelignore'),
160
163
  '.gitignore': await template('gitignore.template'),
161
164
  'mcp-description.md': await template('mcp-description.md'),
@@ -172,7 +175,7 @@ describe('TemplateFilesStep', () => {
172
175
 
173
176
  for (const b of PROTECTED) {
174
177
  const dir = await checkout(b);
175
- for (const rel of ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore', 'mcp-description.md']) {
178
+ for (const rel of ['access.md', '.bevelignore', '.gitignore', 'mcp-description.md']) {
176
179
  expect(await exists(dir, rel), `${b}: ${rel}`).toBe(true);
177
180
  }
178
181
  // The packaged template cannot ship a literal .gitignore (npm strips
@@ -199,12 +202,10 @@ describe('TemplateFilesStep', () => {
199
202
  await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
200
203
 
201
204
  const dir = await checkout(DEFAULT_BRANCH);
202
- const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
203
- expect(agents).toContain('plugins/<Plugin>/plugin.json');
204
- expect(agents).toContain('`docs/`');
205
- expect(agents).not.toContain('{{');
206
- expect(agents).not.toContain('KnowledgeBase/');
205
+ // No guide is written: the platform serves it (see modules/agent-guide).
206
+ expect(await exists(dir, 'AGENTS.md')).toBe(false);
207
207
  const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
208
+ expect(ignore).not.toContain('AGENTS.md');
208
209
  // Both roots stay VISIBLE, whatever they are called: the Skills & Tools
209
210
  // sidebar reads each from the workspace tree.
210
211
  expect(ignore).not.toContain('plugins/');
@@ -228,44 +229,6 @@ describe('TemplateFilesStep', () => {
228
229
  * written file (an MCP session reads the branch's copy of it), under the
229
230
  * deployment's own root names rather than the placeholders.
230
231
  */
231
- it('writes the "Where a new file goes" placement rule, named for the deployment\'s roots', async () => {
232
- kbContext.applyLayout({ knowledgeBaseDir: 'Docs', skillsDir: 'Abilities', pluginsDir: 'Extensions' });
233
- await seedUpstream({ 'marker.txt': 'seeded' });
234
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
235
-
236
- const dir = await checkout(DEFAULT_BRANCH);
237
- const agents = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
238
- const start = agents.indexOf('## Where a new file goes\n');
239
- expect(start, 'the written guide has no placement section').toBeGreaterThan(-1);
240
- // The section alone, so a phrase matched elsewhere in the guide cannot
241
- // stand in for a rule that is missing here.
242
- const section = agents.slice(start).split('\n## ')[0]!;
243
-
244
- // A document goes to the knowledge root, a shared skill to the skills
245
- // root or the plugin's own skills folder, machinery inside a plugin.
246
- expect(section).toContain('`Docs/`');
247
- expect(section).toContain('`Abilities/`');
248
- expect(section).toContain('Extensions/<Plugin>/skills/<skill>/SKILL.md');
249
- expect(section).toContain('Extensions/<Plugin>/software.bevel.hexis/tools/');
250
- for (const kind of ['notes', 'reports', 'tickets', 'mcp.json', 'plugin.json']) {
251
- expect(section, kind).toContain(kind);
252
- }
253
- // And the two rules the tester's agent broke.
254
- expect(section).toMatch(/never holds a document/);
255
- expect(section).toMatch(/\bask\b/);
256
-
257
- // Rendered, not raw: a leftover placeholder would name a folder that is
258
- // not there in a section whose whole job is naming the right folder.
259
- expect(section).not.toContain('{{');
260
- expect(section).not.toContain('KnowledgeBase/');
261
- });
262
-
263
- /**
264
- * The step is built at boot, while the process still runs the defaults; the
265
- * save that completes first-run setup applies the admin's names afterwards.
266
- * A root list snapshotted at construction scaffolded `Skills/` beside the
267
- * `skills/` they had just chosen — the very bug the names exist to avoid.
268
- */
269
232
  it('scaffolds the layout in effect when it RUNS, not the one it was built under', async () => {
270
233
  const builtAtBoot = new TemplateFilesStep(new NodeFs(), kbContext);
271
234
  await seedUpstream({ 'skills/deploy/SKILL.md': '# deploy\n' });
@@ -287,82 +250,6 @@ describe('TemplateFilesStep', () => {
287
250
  expect(subject).not.toContain('Skills/');
288
251
  });
289
252
 
290
- it('replaces a drifted AGENTS.md, and says so when that is the only change', async () => {
291
- await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': 'stale conventions\n' });
292
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
293
-
294
- const dir = await checkout(DEFAULT_BRANCH);
295
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(norm(await template('AGENTS.md')));
296
- const subject = (await git(dir, ['log', '--format=%s', '-1'])).trim();
297
- expect(subject).toBe('Update AGENTS.md to the current platform template');
298
- });
299
-
300
- it('ships the roles-vs-groups guide, and a deployment on an older template receives it at boot', async () => {
301
- const guide = await template('AGENTS.md');
302
- // The section runs from its own heading to the next heading of any level (or the end).
303
- const section = guide.split(/\n(?=#{1,6} )/).find((s) => s.startsWith('### Roles are pre-set')) ?? '';
304
- const prose = section.replace(/\s+/g, ' ');
305
- // What roles are, that agents never create them, the group test, what to do instead.
306
- expect(prose).toContain('A role in `roles.yaml` is an app role');
307
- expect(prose).toContain('**Agents never create roles.**');
308
- expect(prose).toContain('**Is it really a group?**');
309
- expect(prose).toContain('**What to do instead.**');
310
- expect(prose).toContain('add people to existing roles, and use a GROUP for a task- or team-scoped set of people');
311
-
312
- // An existing deployment's AGENTS.md from before the guide existed.
313
- await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': guide.replace(section, '') });
314
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
315
-
316
- const dir = await checkout(DEFAULT_BRANCH);
317
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toContain('**Agents never create roles.**');
318
- });
319
-
320
- it('documents giving a role to a group, with a valid example, in the guide the step writes', async () => {
321
- const guide = await template('AGENTS.md');
322
- const section = guide.split(/\n(?=#{2,3} )/).find((s) => s.startsWith('### Giving a role to a group')) ?? '';
323
- const prose = section.replace(/\s+/g, ' ');
324
- expect(prose).toContain('`- group:<Name>`');
325
- expect(prose).toContain('case- and whitespace-insensitively against the active group source');
326
- expect(prose).toContain('validation error');
327
- expect(prose).toContain('names the entry');
328
- expect(prose).toContain("removes the role's contribution for everyone in the group");
329
- // A bare name resolves to a same-named group first, so role denials use `role/`.
330
- expect(prose).toContain('`deny role/Reviewer`');
331
- // Closeness first: a person's own entry wins only inside the same access.md.
332
- expect(prose).toContain('The nearest `access.md` that says anything about the person decides');
333
- expect(prose).toContain('in the SAME `access.md` as the denial keeps that access');
334
- expect(prose).toContain('A group under `Admin` makes every member a full admin');
335
- // A change request cannot carry a roles.yaml edit — the merge restores the
336
- // base file — so the guide says who changes roles and where, and never
337
- // walks an agent through drafting one.
338
- expect(prose).toContain('Only an Admin changes `roles.yaml`');
339
- expect(prose).toContain('A change request cannot carry the edit');
340
- expect(prose).toContain('Do not propose one');
341
- expect(prose).toContain('`edit_file`');
342
- for (const tool of ['create_branch', 'commit_change', 'open_change_request']) {
343
- expect(prose).not.toContain(`\`${tool}\``);
344
- }
345
-
346
- // The example parses as a roles.yaml whose group entry names a real group.
347
- const example = /```yaml\n([\s\S]*?)```/.exec(section)?.[1] ?? '';
348
- expect(example).toContain('- group:Platform Team');
349
- const parsed = parseRolesYaml(example);
350
- expect(parsed.ok).toBe(true);
351
- if (!parsed.ok) return;
352
- const groups = new Map([['platform team', { displayName: 'Platform Team', emails: new Set(['pat@example.com']) }]]);
353
- expect(mergeGroupsIntoRoles(parsed.index, groups, 'groups.yaml')).toEqual([]);
354
- // The example hands out an ordinary role, never Admin.
355
- expect(parsed.index.byEmail.get('pat@example.com')?.has('reviewer')).toBe(true);
356
- expect(parsed.index.byEmail.get('pat@example.com')?.has('role/admin')).toBe(false);
357
-
358
- // What the step writes — the file agents read through the MCP server — carries it.
359
- await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': guide.replace(section, '') });
360
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
361
- const written = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'));
362
- expect(written).toContain('### Giving a role to a group');
363
- expect(written).toContain(example);
364
- });
365
-
366
253
  it('rejects the git folder — in any spelling — as a reserved root name', async () => {
367
254
  for (const bad of ['.git', '.GIT', '.Git', '.git.', '%2egit']) {
368
255
  expect(() => new TemplateFilesStep(new NodeFs(), kbContext, [bad]), bad).toThrow(/must not name the git folder/);
@@ -375,7 +262,6 @@ describe('TemplateFilesStep', () => {
375
262
  // so the declared content itself must arrive with the rule in it.
376
263
  const customTemplate = path.join(root, 'custom-template');
377
264
  await fs.mkdir(customTemplate, { recursive: true });
378
- await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
379
265
  await fs.writeFile(path.join(customTemplate, '.bevelignore'), '# custom\nMyStuff/\n', 'utf8');
380
266
  const scaffold = await fullScaffold();
381
267
  delete scaffold['.bevelignore']; // the one file the step will declare from the template
@@ -386,7 +272,7 @@ describe('TemplateFilesStep', () => {
386
272
  const dir = await checkout(DEFAULT_BRANCH);
387
273
  const lines = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
388
274
  expect(lines).toContain('MyStuff/'); // the operator's rules survive
389
- expect(lines).toContain('AGENTS.md'); // the platform's rule was appended
275
+ expect(lines).not.toContain('AGENTS.md'); // no rule for the guide: it is not on disk
390
276
  expect(lines).toContain('/mcp-description.md');
391
277
  expect(lines).not.toContain('Skills/'); // never the skills root — the Library's tree needs it
392
278
  });
@@ -531,7 +417,7 @@ describe('TemplateFilesStep', () => {
531
417
  await expect(fs.readFile(path.join(dir, 'notes.md'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
532
418
  await expect(fs.readdir(path.join(dir, '.git'))).rejects.toMatchObject({ code: 'ENOENT' });
533
419
  // The rest of the template still seeded.
534
- expect(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')).toContain('#');
420
+ expect(await fs.readFile(path.join(dir, 'access.md'), 'utf8')).toContain('---');
535
421
  });
536
422
 
537
423
  it('seeds a binary template file byte for byte and keeps a script executable — text is what decodes', async () => {
@@ -564,7 +450,6 @@ describe('TemplateFilesStep', () => {
564
450
  // must arrive already reconciled.
565
451
  const customTemplate = path.join(root, 'custom-template-stale');
566
452
  await fs.mkdir(customTemplate, { recursive: true });
567
- await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
568
453
  await fs.writeFile(
569
454
  path.join(customTemplate, '.bevelignore'),
570
455
  '# custom\nMyStuff/\n# The shared-skills root is rendered by the Skills & Tools app, like {{pluginsDir}}/.\n{{skillsDir}}/\n',
@@ -717,7 +602,6 @@ describe('TemplateFilesStep', () => {
717
602
  // comment. The packaged copy stands in, and the log says so once.
718
603
  const customTemplate = path.join(root, 'custom-template-old');
719
604
  await fs.mkdir(customTemplate, { recursive: true });
720
- await fs.writeFile(path.join(customTemplate, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
721
605
  await fs.writeFile(path.join(customTemplate, 'access.md'), await template('access.md'), 'utf8');
722
606
  const scaffold = await fullScaffold();
723
607
  delete scaffold['mcp-description.md'];
@@ -736,327 +620,169 @@ describe('TemplateFilesStep', () => {
736
620
  it('gives the stand-in to that file only: a custom template missing access.md still fails loudly', async () => {
737
621
  const stricter = path.join(root, 'custom-template-broken');
738
622
  await fs.mkdir(stricter, { recursive: true });
739
- await fs.writeFile(path.join(stricter, 'AGENTS.md'), await template('AGENTS.md'), 'utf8');
740
623
  const scaffold = await fullScaffold();
741
624
  delete scaffold['access.md'];
742
625
  await seedUpstream(scaffold);
743
626
  await expect(makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)], stricter).runAll()).rejects.toThrow(/ENOENT/);
744
627
  });
628
+ });
745
629
 
746
- it('points a clone at mcp-description.md from the managed AGENTS.md, before the platform mechanics', async () => {
747
- const agents = await template('AGENTS.md');
748
- const pointer = agents.indexOf('mcp-description.md');
749
- expect(pointer).toBeGreaterThan(-1);
750
- expect(pointer).toBeLessThan(agents.indexOf('## Directory Structure'));
751
- expect(agents).toMatch(/default branch's copy inline/);
752
- });
753
-
754
- it('treats a CRLF checkout of identical AGENTS.md content as current — no churn commit', async () => {
755
- const scaffold = await fullScaffold();
756
- scaffold['AGENTS.md'] = norm(scaffold['AGENTS.md']!).replace(/\n/g, '\r\n');
757
- await seedUpstream(scaffold);
630
+ /**
631
+ * The guide copies earlier releases wrote to the repository root, and the
632
+ * ignore rules that hid them. The guide is served from code now (see
633
+ * modules/agent-guide), so a copy on disk is a stale file under a header
634
+ * that says the platform owns it — taken out on the first start, on every
635
+ * protected branch, and only when that header proves it ours.
636
+ */
637
+ describe('TemplateFilesStep: the guide copies earlier releases wrote', () => {
638
+ /** The ignore file the last release that wrote the guide shipped, verbatim. */
639
+ const OLD_RELEASE_IGNORE =
640
+ '# Entries listed here are hidden from the Bevel file tree / agent view.\n\n.git/\n\n' +
641
+ '# Repo hygiene files that clutter the tree without being node content.\n.gitignore\n.gitattributes\n' +
642
+ "# The platform's agent guide.\nAGENTS.md\n" +
643
+ '# The deployment preamble is edited from External agent access, not as a KB page.\n/mcp-description.md\n' +
644
+ '# The pre-rename name. Listed so a knowledge base carrying both files hides\n' +
645
+ '# both — top-up adds AGENTS.md but never deletes the CLAUDE.md beside it.\nCLAUDE.md\n\n' +
646
+ '**/.gitkeep\n\n**/access.md\nroles.yaml\n';
647
+ const CUSTOMER_GUIDE = '# Acme conventions\n\nWrite tickets in the present tense.\n';
648
+ const ignoreLines = async (dir: string) =>
649
+ norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
650
+
651
+ it('removes the platform-written AGENTS.md from every protected branch, and the rules that hid it', async () => {
652
+ await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': MANAGED_GUIDE_COPY, '.bevelignore': OLD_RELEASE_IGNORE });
758
653
  await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
759
654
 
760
- const dir = await checkout(DEFAULT_BRANCH);
761
- expect((await git(dir, ['rev-list', '--count', 'HEAD'])).trim()).toBe('1'); // init only
762
- });
763
-
764
- /**
765
- * The guide under a name of the deployment's own — and what becomes of the
766
- * `AGENTS.md` beside it, which on such a deployment is the CUSTOMER'S file.
767
- */
768
- describe('a renamed agent guide', () => {
769
- /** The scaffold minus the guide, so each test says for itself what `AGENTS.md` holds. */
770
- async function scaffoldWithoutGuide(): Promise<Record<string, string>> {
771
- const scaffold = await fullScaffold();
772
- delete scaffold['AGENTS.md'];
773
- return scaffold;
774
- }
775
-
776
- const CUSTOMER_GUIDE = '# Acme conventions\n\nWrite tickets in the present tense.\n';
777
-
778
- it('writes and refreshes the guide under the configured name, on every protected branch', async () => {
779
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
780
- await seedUpstream({ 'marker.txt': 'seeded' });
781
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
782
-
783
- for (const branch of PROTECTED) {
784
- const dir = await checkout(branch);
785
- const guide = norm(await fs.readFile(path.join(dir, 'HEXIS.md'), 'utf8'));
786
- // The header names the file it lives in, not the name it ships under.
787
- expect(guide).toContain('`HEXIS.md`');
788
- expect(guide).not.toContain('{{');
789
- expect(await exists(dir, 'AGENTS.md')).toBe(false);
790
- }
791
-
792
- // A second boot sees the rendered guide as current: no churn commit…
793
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
794
- expect((await git(await checkout(DEFAULT_BRANCH), ['rev-list', '--count', 'HEAD'])).trim()).toBe('2');
795
-
796
- // …and a drifted copy is replaced, exactly as `AGENTS.md` is by default.
797
- const dir = await checkout(DEFAULT_BRANCH);
798
- await fs.writeFile(path.join(dir, 'HEXIS.md'), 'stale conventions\n', 'utf8');
799
- await git(dir, ['commit', '-am', 'drift']);
800
- await git(dir, ['push', 'origin', DEFAULT_BRANCH]);
801
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
802
- const after = await checkout(DEFAULT_BRANCH);
803
- expect(norm(await fs.readFile(path.join(after, 'HEXIS.md'), 'utf8'))).toContain('# Knowledge base');
804
- expect((await git(after, ['log', '--format=%s', '-1'])).trim()).toBe(
805
- 'Update HEXIS.md to the current platform template',
806
- );
807
- });
808
-
809
- it('leaves a customer AGENTS.md byte for byte alone, and shows it in the tree', async () => {
810
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
811
- // Their file already names the guide, so the pointer has nothing to add
812
- // — this test is about the bytes and the ignore rules alone.
813
- const theirs = `${CUSTOMER_GUIDE}\nSee HEXIS.md for the platform.\n`;
814
- await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': theirs });
815
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
816
-
817
- const dir = await checkout(DEFAULT_BRANCH);
818
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(theirs);
819
- expect(await exists(dir, 'HEXIS.md')).toBe(true);
820
- const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8'))
821
- .split('\n')
822
- .map((l) => l.trim());
823
- // The configured file is hidden; the customer's shows.
824
- expect(ignore).toContain('HEXIS.md');
825
- expect(ignore).not.toContain('AGENTS.md');
826
- });
827
-
828
- it('removes the platform-written AGENTS.md — only its own header proves it ours', async () => {
829
- // A knowledge base the platform seeded, whose admin then renames the guide.
830
- await seedUpstream({ 'marker.txt': 'seeded' });
831
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
832
- expect(await exists(await checkout(DEFAULT_BRANCH), 'AGENTS.md')).toBe(true);
833
-
834
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
835
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
836
-
837
- // On EVERY protected branch: the step tops up each one, and a rename
838
- // that only reached the default branch would leave the others carrying a
839
- // stale platform guide under the customer's name.
840
- for (const branch of PROTECTED) {
841
- const dir = await checkout(branch);
842
- expect(await exists(dir, 'AGENTS.md'), branch).toBe(false);
843
- expect(await exists(dir, 'HEXIS.md'), branch).toBe(true);
844
- const message = await git(dir, ['log', '--format=%B', '-1']);
845
- expect(message, branch).toContain(
846
- 'Remove the platform-written AGENTS.md — the agent guide is now HEXIS.md',
847
- );
848
- }
849
- });
850
-
851
- it('keeps an AGENTS.md it cannot prove it wrote, and says so', async () => {
852
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
853
- // Edited by the customer: the managed header is gone, so it is theirs.
854
- const edited = `${CUSTOMER_GUIDE}\nMentions HEXIS.md already.\n`;
855
- await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': edited });
856
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
857
-
858
- for (const branch of PROTECTED) {
859
- const dir = await checkout(branch);
860
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8')), branch).toBe(edited);
861
- expect(await git(dir, ['log', '--format=%B', '-1']), branch).toContain('Keep AGENTS.md');
862
- }
863
- });
864
-
865
- it('leaves a root AGENTS.md that is a symbolic link exactly where it points', async () => {
866
- // Following the link would read the managed header at the other end and
867
- // delete the customer's entry — or, with the pointer on, write our
868
- // sentence into a file nobody asked us to edit.
869
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
870
- await seedUpstream({ ...(await scaffoldWithoutGuide()), 'conventions/ours.md': await template('AGENTS.md') });
871
- // Committed as a link, so every checkout the step makes carries one.
872
- const seeding = await checkout(DEFAULT_BRANCH);
873
- await fs.symlink('conventions/ours.md', path.join(seeding, 'AGENTS.md'));
874
- await git(seeding, ['add', '-A']);
875
- await git(seeding, ['commit', '-m', 'link AGENTS.md at our own file']);
876
- await git(seeding, ['push', 'origin', DEFAULT_BRANCH]);
877
-
878
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
879
-
880
- const dir = await checkout(DEFAULT_BRANCH);
881
- expect((await fs.lstat(path.join(dir, 'AGENTS.md'))).isSymbolicLink()).toBe(true);
882
- expect(await fs.readlink(path.join(dir, 'AGENTS.md'))).toBe('conventions/ours.md');
883
- // The file at the other end is untouched — neither removed nor appended to.
884
- expect(norm(await fs.readFile(path.join(dir, 'conventions/ours.md'), 'utf8'))).toBe(
885
- norm(await template('AGENTS.md')),
886
- );
887
- });
888
-
889
- it("keeps an operator's own AGENTS.md ignore rule while dropping the platform's", async () => {
890
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
891
- const scaffold = await scaffoldWithoutGuide();
892
- // Their own rule, written by hand, nowhere near the template's slot.
893
- scaffold['.bevelignore'] = '# mine\nAGENTS.md\nMyStuff/\n';
894
- scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
895
- await seedUpstream(scaffold);
896
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
897
-
898
- const text = norm(
899
- await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
900
- );
901
- expect(text).toContain('# mine\nAGENTS.md\nMyStuff/');
902
- expect(text.split('\n').map((l) => l.trim())).toContain('HEXIS.md');
903
- });
904
-
905
- it("keeps an operator's rule that merely happens to sit under .gitattributes", async () => {
906
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
907
- const scaffold = await scaffoldWithoutGuide();
908
- // Hand-written, and by coincidence in the position the template's own
909
- // rule used to occupy. One matching line above is not provenance — the
910
- // block the template wrote is not there, so the rule is theirs.
911
- scaffold['.bevelignore'] = '# ours\n.gitattributes\nAGENTS.md\nMyStuff/\n';
912
- scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
913
- await seedUpstream(scaffold);
914
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
915
-
916
- const text = norm(
917
- await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
918
- );
919
- expect(text).toContain('# ours\n.gitattributes\nAGENTS.md\nMyStuff/');
920
- expect(text.split('\n').map((l) => l.trim())).toContain('HEXIS.md');
921
- });
922
-
923
- it("drops the bare rule an earlier release seeded in the template's hygiene block", async () => {
924
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
925
- const scaffold = await scaffoldWithoutGuide();
926
- // The `.bevelignore` of a knowledge base seeded before the rule had a
927
- // comment above it: bare, at the end of the template's own block.
928
- scaffold['.bevelignore'] =
929
- '# Repo hygiene files that clutter the tree without being node content.\n' +
930
- '.gitignore\n.gitattributes\nAGENTS.md\nroles.yaml\n';
931
- scaffold['AGENTS.md'] = `${CUSTOMER_GUIDE}\nSee HEXIS.md.\n`;
932
- await seedUpstream(scaffold);
933
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
934
-
935
- const lines = norm(
936
- await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'),
937
- )
938
- .split('\n')
939
- .map((l) => l.trim());
940
- // The platform's line goes; the block above it is content and stays.
655
+ for (const branch of PROTECTED) {
656
+ const dir = await checkout(branch);
657
+ expect(await exists(dir, 'AGENTS.md'), branch).toBe(false);
658
+ const lines = await ignoreLines(dir);
941
659
  expect(lines).not.toContain('AGENTS.md');
660
+ expect(lines).not.toContain('CLAUDE.md');
661
+ expect(lines).not.toContain("# The platform's agent guide.");
662
+ expect(lines).not.toContain('# The pre-rename name. Listed so a knowledge base carrying both files hides');
663
+ // Everything else in the file is as it was.
942
664
  expect(lines).toContain('.gitattributes');
943
- expect(lines).toContain('.gitignore');
944
- expect(lines).toContain('HEXIS.md');
945
- });
665
+ expect(lines).toContain('/mcp-description.md');
666
+ expect(lines).toContain('roles.yaml');
667
+ expect(await git(dir, ['log', '--format=%B', '-1']), branch).toContain(
668
+ 'Remove the platform-written AGENTS.md — the agent guide is served by the platform now',
669
+ );
670
+ }
946
671
 
947
- describe('the pointer in the customer\'s own AGENTS.md', () => {
948
- /** Boot once with the guide renamed, against a root `AGENTS.md` holding `theirs`. */
949
- async function bootWith(theirs: string | null, link = true): Promise<string> {
950
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
951
- const scaffold = await scaffoldWithoutGuide();
952
- if (theirs !== null) scaffold['AGENTS.md'] = theirs;
953
- await seedUpstream(scaffold);
954
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext, [], () => link)]).runAll();
955
- return checkout(DEFAULT_BRANCH);
956
- }
672
+ // The boot after finds nothing to do.
673
+ const head = (await git(await checkout(DEFAULT_BRANCH), ['rev-parse', 'HEAD'])).trim();
674
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
675
+ expect((await git(await checkout(DEFAULT_BRANCH), ['rev-parse', 'HEAD'])).trim()).toBe(head);
676
+ });
957
677
 
958
- const SENTENCE = agentsFilePointerSentence('HEXIS.md');
678
+ it("leaves the organisation's own AGENTS.md byte for byte, and stops hiding it", async () => {
679
+ await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': CUSTOMER_GUIDE, '.bevelignore': OLD_RELEASE_IGNORE });
680
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
959
681
 
960
- it('appends the sentence when the guide is not mentioned, and says so', async () => {
961
- const dir = await bootWith(CUSTOMER_GUIDE);
962
- const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
963
- // Their bytes, then a blank line, then exactly the shared sentence.
964
- expect(text).toBe(`${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${SENTENCE}\n`);
965
- expect(await git(dir, ['log', '--format=%B', '-1'])).toContain(
966
- 'Add a pointer to HEXIS.md at the end of AGENTS.md',
967
- );
968
- });
682
+ const dir = await checkout(DEFAULT_BRANCH);
683
+ expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(CUSTOMER_GUIDE);
684
+ expect(await ignoreLines(dir)).not.toContain('AGENTS.md');
685
+ expect(await git(dir, ['log', '--format=%s', '-1'])).toContain('.bevelignore');
686
+ });
969
687
 
970
- /**
971
- * The sentence spells a punctuated name twice and neither spelling is
972
- * the raw one — the label is escaped, the destination percent-encoded.
973
- * A second boot must still see the guide as mentioned, or it appends
974
- * another copy, and the boot after that a third.
975
- */
976
- it('appends once and only once, even when the name is nothing but punctuation', async () => {
977
- const guide = 'Our [Agent] Guide (v2).md';
978
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: guide });
979
- await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': CUSTOMER_GUIDE });
980
- const boot = () => makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
981
-
982
- await boot();
983
- const once = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'));
984
- expect(once).toBe(
985
- `${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${agentsFilePointerSentence(guide)}\n`,
986
- );
987
-
988
- const head = (await git(await checkout(DEFAULT_BRANCH), ['rev-parse', 'HEAD'])).trim();
989
- await boot();
990
- const after = await checkout(DEFAULT_BRANCH);
991
- expect(norm(await fs.readFile(path.join(after, 'AGENTS.md'), 'utf8'))).toBe(once);
992
- // And the second boot committed nothing at all.
993
- expect((await git(after, ['rev-parse', 'HEAD'])).trim()).toBe(head);
994
- });
688
+ it("stops hiding the organisation's own file under a name a deployment once gave the guide, with no copy left to find it by", async () => {
689
+ // A deployment saved `HEXIS.md` for the guide; the organisation then made
690
+ // that file its own (no platform header left), and the rule the platform
691
+ // appended for the name is still hiding it. The saved name is not read
692
+ // any more, and no managed copy names it either: the rule is known by the
693
+ // comment the platform wrote above it, under either of its two spellings.
694
+ const scaffold = await fullScaffold();
695
+ scaffold['HEXIS.md'] = CUSTOMER_GUIDE;
696
+ scaffold['Guide.md'] = CUSTOMER_GUIDE;
697
+ scaffold['.bevelignore'] =
698
+ `${OLD_RELEASE_IGNORE}\n# The platform's agent guide.\nHEXIS.md\n` +
699
+ '\n# Added by the platform: the conventions doc is not node content.\nGuide.md\n' +
700
+ // A negation and a hand-written rule under neither comment stay.
701
+ '!Notes.md\n# mine\nPrivate.md\n';
702
+ await seedUpstream(scaffold);
703
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
995
704
 
996
- /**
997
- * The guide's name is a setting, and a setting can be changed twice. The
998
- * sentence the last rename wrote points at a file that is no longer
999
- * there, and the new name is nowhere in the text — so "is it mentioned?"
1000
- * says no and an unaimed sentence would be joined by a second one.
1001
- */
1002
- it('aims the sentence it already wrote at the new guide, rather than adding another', async () => {
1003
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
1004
- await seedUpstream({ ...(await scaffoldWithoutGuide()), 'AGENTS.md': CUSTOMER_GUIDE });
1005
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1006
- expect(norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), 'AGENTS.md'), 'utf8'))).toContain(
1007
- agentsFilePointerSentence('HEXIS.md'),
1008
- );
1009
-
1010
- // The admin renames the guide a second time.
1011
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'GUIDE.md' });
1012
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1013
-
1014
- const dir = await checkout(DEFAULT_BRANCH);
1015
- const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
1016
- // Their bytes, then ONE sentence, pointing at the guide that exists.
1017
- expect(text).toBe(
1018
- `${CUSTOMER_GUIDE.replace(/\n+$/, '')}\n\n${agentsFilePointerSentence('GUIDE.md')}\n`,
1019
- );
1020
- expect(text).not.toContain('HEXIS.md');
1021
- expect(await git(dir, ['log', '--format=%B', '-1'])).toContain(
1022
- 'Point the sentence in AGENTS.md at GUIDE.md',
1023
- );
1024
- });
705
+ const dir = await checkout(DEFAULT_BRANCH);
706
+ expect(norm(await fs.readFile(path.join(dir, 'HEXIS.md'), 'utf8'))).toBe(CUSTOMER_GUIDE);
707
+ expect(norm(await fs.readFile(path.join(dir, 'Guide.md'), 'utf8'))).toBe(CUSTOMER_GUIDE);
708
+ const lines = await ignoreLines(dir);
709
+ expect(lines).not.toContain('HEXIS.md');
710
+ expect(lines).not.toContain('Guide.md');
711
+ expect(lines).not.toContain("# The platform's agent guide.");
712
+ expect(lines).not.toContain('# Added by the platform: the conventions doc is not node content.');
713
+ expect(lines).toContain('!Notes.md');
714
+ expect(lines).toContain('# mine');
715
+ expect(lines).toContain('Private.md');
716
+ });
717
+
718
+ it("keeps a rule the operator wrote by hand — provenance is the platform's comment or its template slot", async () => {
719
+ const scaffold = await fullScaffold();
720
+ scaffold['.bevelignore'] = '# mine\nAGENTS.md\nMyStuff/\n';
721
+ scaffold['AGENTS.md'] = CUSTOMER_GUIDE;
722
+ await seedUpstream(scaffold);
723
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
724
+ const text = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'));
725
+ expect(text).toContain('# mine\nAGENTS.md\nMyStuff/');
726
+ });
1025
727
 
1026
- it('writes nothing when the name appears anywhere in the text, in any wording', async () => {
1027
- const theirs = `${CUSTOMER_GUIDE}\nThe platform keeps its own notes in HEXIS.md; read those too.\n`;
1028
- const dir = await bootWith(theirs);
1029
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(theirs);
1030
- });
728
+ it('removes a copy under the name a deployment saved for the guide, its escaped rule, and a CLAUDE.md copy', async () => {
729
+ kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: '#Guide[1].md' });
730
+ const scaffold = await fullScaffold();
731
+ scaffold['#Guide[1].md'] = MANAGED_GUIDE_COPY;
732
+ scaffold['CLAUDE.md'] = MANAGED_GUIDE_COPY;
733
+ // The rule the release that wrote the guide under that name appended.
734
+ scaffold['.bevelignore'] = `${OLD_RELEASE_IGNORE}\n# The platform's agent guide.\n${String.raw`\#Guide\[1\].md`}\n`;
735
+ await seedUpstream(scaffold);
736
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1031
737
 
1032
- it('writes nothing while the setting is off', async () => {
1033
- const dir = await bootWith(CUSTOMER_GUIDE, false);
1034
- expect(norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'))).toBe(CUSTOMER_GUIDE);
1035
- });
738
+ const dir = await checkout(DEFAULT_BRANCH);
739
+ expect(await exists(dir, '#Guide[1].md')).toBe(false);
740
+ expect(await exists(dir, 'CLAUDE.md')).toBe(false);
741
+ const lines = await ignoreLines(dir);
742
+ expect(lines).not.toContain(String.raw`\#Guide\[1\].md`);
743
+ expect(lines).not.toContain('AGENTS.md');
744
+ expect(lines).not.toContain('CLAUDE.md');
745
+ expect(await git(dir, ['log', '--format=%B', '-1'])).toContain('Remove the platform-written #Guide[1].md and CLAUDE.md');
746
+ });
747
+
748
+ it('finds a copy under a name the deployment no longer uses, and leaves a root note of the organisation\'s own', async () => {
749
+ // Renamed twice while the guide was a file: a copy under the first name
750
+ // is still there, hidden by the rule that release appended, and nothing
751
+ // in today's settings remembers the name. The header does.
752
+ const scaffold = await fullScaffold();
753
+ scaffold['OLD-GUIDE.md'] = MANAGED_GUIDE_COPY;
754
+ scaffold['Welcome.md'] = '# Welcome\n\nStart with the handbook.\n';
755
+ scaffold['.bevelignore'] = `${OLD_RELEASE_IGNORE}\n# The platform's agent guide.\nOLD-GUIDE.md\n`;
756
+ await seedUpstream(scaffold);
757
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1036
758
 
1037
- it('never creates an AGENTS.md for the pointer', async () => {
1038
- const dir = await bootWith(null);
1039
- expect(await exists(dir, 'AGENTS.md')).toBe(false);
1040
- expect(await exists(dir, 'HEXIS.md')).toBe(true);
1041
- });
759
+ const dir = await checkout(DEFAULT_BRANCH);
760
+ expect(await exists(dir, 'OLD-GUIDE.md')).toBe(false);
761
+ expect(norm(await fs.readFile(path.join(dir, 'Welcome.md'), 'utf8'))).toBe('# Welcome\n\nStart with the handbook.\n');
762
+ const lines = await ignoreLines(dir);
763
+ expect(lines).not.toContain('OLD-GUIDE.md');
764
+ expect(lines).not.toContain('AGENTS.md');
765
+ expect(await git(dir, ['log', '--format=%B', '-1'])).toContain('Remove the platform-written OLD-GUIDE.md');
766
+ });
1042
767
 
1043
- it('never appends to a file the platform wrote — it removes that one instead', async () => {
1044
- const dir = await bootWith(await template('AGENTS.md'));
1045
- expect(await exists(dir, 'AGENTS.md')).toBe(false);
1046
- });
768
+ it('leaves a root AGENTS.md that is a symbolic link exactly where it points', async () => {
769
+ // Following the link would read the managed header at the other end and
770
+ // delete the organisation's entry.
771
+ const scaffold = await fullScaffold();
772
+ scaffold['conventions/ours.md'] = MANAGED_GUIDE_COPY;
773
+ await seedUpstream(scaffold);
774
+ const seeding = await checkout(DEFAULT_BRANCH);
775
+ await fs.symlink('conventions/ours.md', path.join(seeding, 'AGENTS.md'));
776
+ await git(seeding, ['add', '-A']);
777
+ await git(seeding, ['commit', '-m', 'link AGENTS.md at our own file']);
778
+ await git(seeding, ['push', 'origin', DEFAULT_BRANCH]);
1047
779
 
1048
- it('does nothing at all under the default name', async () => {
1049
- const theirs = `${CUSTOMER_GUIDE}`;
1050
- // The default guide IS AGENTS.md, so the platform owns that file and
1051
- // the pointer question never arises: the drifted copy is replaced.
1052
- await seedUpstream({ ...(await fullScaffold()), 'AGENTS.md': theirs });
1053
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1054
- const dir = await checkout(DEFAULT_BRANCH);
1055
- const text = norm(await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8'));
1056
- expect(text).toBe(norm(await template('AGENTS.md')));
1057
- expect(text).not.toContain(agentsFilePointerSentence('AGENTS.md'));
1058
- });
1059
- });
780
+ await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
781
+
782
+ const dir = await checkout(DEFAULT_BRANCH);
783
+ expect((await fs.lstat(path.join(dir, 'AGENTS.md'))).isSymbolicLink()).toBe(true);
784
+ expect(await fs.readlink(path.join(dir, 'AGENTS.md'))).toBe('conventions/ours.md');
785
+ expect(norm(await fs.readFile(path.join(dir, 'conventions/ours.md'), 'utf8'))).toBe(MANAGED_GUIDE_COPY);
1060
786
  });
1061
787
  });
1062
788
 
@@ -1160,47 +886,46 @@ describe('buildSeedTree', () => {
1160
886
  expect(generated.sort()).toEqual(['Docs/.gitkeep', 'Plugins/.gitkeep', 'roles.yaml', 'skills/.gitkeep']);
1161
887
  });
1162
888
 
1163
- /**
1164
- * An empty remote seeded by a deployment that renamed its guide. Done in the
1165
- * SEED rather than left to the top-up step, which would otherwise lay down
1166
- * `AGENTS.md` and delete it one commit later — two commits saying opposite
1167
- * things about a knowledge base nobody has used yet.
1168
- */
1169
- it('seeds the packaged guide under the configured name, and no AGENTS.md beside it', async () => {
1170
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
1171
- const dest = path.join(root, 'seed-dest-guide');
889
+ it('seeds no guide, and no rule hiding one: the platform serves the guide', async () => {
890
+ const dest = path.join(root, 'seed-dest-no-guide');
1172
891
  await fs.mkdir(dest, { recursive: true });
1173
892
  await buildSeedTree(new NodeFs(), TEMPLATE_DIR, [], ['admin@example.com'], kbContext)(dest);
1174
893
  expect(await exists(dest, 'AGENTS.md')).toBe(false);
1175
- const guide = norm(await fs.readFile(path.join(dest, 'HEXIS.md'), 'utf8'));
1176
- expect(guide).toContain('`HEXIS.md`');
1177
- expect(guide).not.toContain('{{');
1178
- // And the ignore file hides the name it actually seeded.
1179
- const ignore = norm(await fs.readFile(path.join(dest, '.bevelignore'), 'utf8'))
1180
- .split('\n')
1181
- .map((l) => l.trim());
1182
- expect(ignore).toContain('HEXIS.md');
894
+ expect(await exists(dest, 'CLAUDE.md')).toBe(false);
895
+ const ignore = norm(await fs.readFile(path.join(dest, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
1183
896
  expect(ignore).not.toContain('AGENTS.md');
1184
- });
1185
-
1186
- it('lets the managed guide win a template that carries a root file of the same name', async () => {
1187
- // A custom KB_TEMPLATE_DIR that happens to ship `HEXIS.md` at its root, on
1188
- // a deployment that named its guide `HEXIS.md`: two walk entries, one
1189
- // destination. Whichever the walk reached last used to decide what an
1190
- // empty deployment was seeded with. The managed guide decides.
1191
- const customTemplate = path.join(root, 'custom-template-guide-clash');
1192
- await fs.cp(TEMPLATE_DIR, customTemplate, { recursive: true });
1193
- await fs.writeFile(path.join(customTemplate, 'HEXIS.md'), '# Not the platform guide\n', 'utf8');
1194
-
1195
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' });
1196
- const dest = path.join(root, 'seed-dest-guide-clash');
1197
- await fs.mkdir(dest, { recursive: true });
1198
- await buildSeedTree(new NodeFs(), customTemplate, [], ['admin@example.com'], kbContext)(dest);
1199
-
1200
- const guide = norm(await fs.readFile(path.join(dest, 'HEXIS.md'), 'utf8'));
1201
- expect(guide).toContain('`HEXIS.md`');
1202
- expect(guide).not.toContain('Not the platform guide');
1203
- expect(await exists(dest, 'AGENTS.md')).toBe(false);
897
+ expect(ignore).not.toContain('CLAUDE.md');
898
+ expect(ignore).toContain('/mcp-description.md');
899
+ });
900
+
901
+ it("skips a distribution template's root AGENTS.md that is the platform's old guide, and seeds one that is the organisation's own", async () => {
902
+ // A KB_TEMPLATE_DIR forked while the guide was a file still ships one. Seeded,
903
+ // the first start would remove it one commit later — two commits saying
904
+ // opposite things about a knowledge base nobody has used yet.
905
+ const stale = path.join(root, 'custom-template-stale-guide');
906
+ await fs.cp(TEMPLATE_DIR, stale, { recursive: true });
907
+ await fs.writeFile(path.join(stale, 'AGENTS.md'), MANAGED_GUIDE_COPY, 'utf8');
908
+ // Under a name a deployment once gave the guide, too: the copy is known
909
+ // by its header, under any root Markdown name, as the first start knows it.
910
+ await fs.writeFile(path.join(stale, 'OLD-GUIDE.md'), MANAGED_GUIDE_COPY, 'utf8');
911
+ // And whatever case the extension was saved in, as the first start judges it.
912
+ await fs.writeFile(path.join(stale, 'LEGACY-GUIDE.MD'), MANAGED_GUIDE_COPY, 'utf8');
913
+ const staleDest = path.join(root, 'seed-dest-stale-guide');
914
+ await fs.mkdir(staleDest, { recursive: true });
915
+ await buildSeedTree(new NodeFs(), stale, [], ['admin@example.com'], kbContext)(staleDest);
916
+ expect(await exists(staleDest, 'AGENTS.md')).toBe(false);
917
+ expect(await exists(staleDest, 'OLD-GUIDE.md')).toBe(false);
918
+ expect(await exists(staleDest, 'LEGACY-GUIDE.MD')).toBe(false);
919
+
920
+ // A template whose root AGENTS.md is the organisation's own text is content,
921
+ // and is seeded as the content it is.
922
+ const own = path.join(root, 'custom-template-own-guide');
923
+ await fs.cp(TEMPLATE_DIR, own, { recursive: true });
924
+ await fs.writeFile(path.join(own, 'AGENTS.md'), '# Acme conventions\n', 'utf8');
925
+ const ownDest = path.join(root, 'seed-dest-own-guide');
926
+ await fs.mkdir(ownDest, { recursive: true });
927
+ await buildSeedTree(new NodeFs(), own, [], ['admin@example.com'], kbContext)(ownDest);
928
+ expect(norm(await fs.readFile(path.join(ownDest, 'AGENTS.md'), 'utf8'))).toBe('# Acme conventions\n');
1204
929
  });
1205
930
 
1206
931
  it('names a template that is not there — a missing directory is a broken build, not an empty seed', async () => {
@@ -1974,26 +1699,3 @@ describe('GroupsToPluginsStep — migration edge cases', () => {
1974
1699
  });
1975
1700
  });
1976
1701
  });
1977
-
1978
- /**
1979
- * The guide's rule in `.bevelignore` is a gitignore PATTERN, and the name rules
1980
- * admit characters gitignore reads as syntax: written bare, `#Guide.md` is a
1981
- * comment and hides nothing. Rendered escaped, the rule names exactly the file.
1982
- */
1983
- describe('TemplateFilesStep: a guide named with gitignore syntax', () => {
1984
- it('escapes the name in the ignore rule it writes, and keeps it escaped on the next boot', async () => {
1985
- kbContext.applyLayout({ ...DEFAULT_KB_LAYOUT, agentsFile: '#Guide[1].md' });
1986
- await seedUpstream({ 'marker.txt': 'seeded' });
1987
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1988
- const dir = await checkout(DEFAULT_BRANCH);
1989
- const ignore = norm(await fs.readFile(path.join(dir, '.bevelignore'), 'utf8')).split('\n').map((l) => l.trim());
1990
- const escaped = String.raw`\#Guide\[1\].md`;
1991
- expect(ignore).toContain(escaped);
1992
- expect(ignore).not.toContain('#Guide[1].md');
1993
- expect(await exists(dir, '#Guide[1].md')).toBe(true);
1994
- // A second boot sees the escaped rule as present and adds no second line.
1995
- await makeRunner([new TemplateFilesStep(new NodeFs(), kbContext)]).runAll();
1996
- const again = norm(await fs.readFile(path.join(await checkout(DEFAULT_BRANCH), '.bevelignore'), 'utf8'));
1997
- expect(again.split('\n').filter((l) => l.trim() === escaped)).toHaveLength(1);
1998
- });
1999
- });