@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
@@ -2,23 +2,29 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  LEGACY_AGENTS_FILE,
5
- agentsFilePointerSentence,
6
- gitignoreLiteral,
7
- mentionsAgentsFile,
8
- retargetAgentsFilePointer,
9
5
  validateKbRootName,
10
6
  type KbLayout,
11
7
  } from '@bevel-software/platform-shared';
12
8
  import { IGNORE_FILENAME, isAbsence, type IFsProbe } from '../../../../shared/fs.contract.js';
13
9
  import type { KbContext } from '../../../../shared/kb-context.js';
14
10
  import { PREAMBLE_FILE } from '../../../agent-instructions/compose.js';
11
+ import { isManagedGuide } from '../../../agent-guide/agent-guide.js';
15
12
  import { TemplateSource } from './template-source.js';
16
13
  import type { KbBranch, OnServerStart, ServerStartContext, StepResult } from '../on-server-start.js';
17
14
  import { hasGitInternalsSegment } from '../../../../shared/git-internals.js';
18
15
 
16
+ export { isManagedGuide };
17
+
19
18
  /** Root-anchored so a knowledge folder may still contain an ordinary namesake. */
20
19
  const PREAMBLE_IGNORE_PATTERN = `/${PREAMBLE_FILE}`;
21
20
 
21
+ /**
22
+ * The guide's name before it was `AGENTS.md`, back when it was a file. A copy
23
+ * the platform wrote under it is taken out of the repository like one under
24
+ * the current name (see {@link TemplateFilesStep.retireGuideCopies}).
25
+ */
26
+ const PRE_RENAME_AGENTS_FILE = 'CLAUDE.md';
27
+
22
28
  /**
23
29
  * The **required scaffolding** — the minimum an operational KB needs. Any of
24
30
  * these missing from a protected branch are added at the startup phase; the
@@ -34,16 +40,14 @@ const PREAMBLE_IGNORE_PATTERN = `/${PREAMBLE_FILE}`;
34
40
  *
35
41
  * `roles.yaml` is in neither, and is not part of the template at all: it is
36
42
  * generated from `ADMIN_EMAIL` (see roles-yaml.step.ts), so a repo can't be
37
- * seeded with a stale hard-coded Admin list.
43
+ * seeded with a stale hard-coded Admin list. The agent guide is in neither
44
+ * either, any more: it is served from code (see modules/agent-guide), and the
45
+ * copies earlier releases wrote are REMOVED here, not refreshed.
38
46
  */
39
- export function requiredFiles(layout: Required<KbLayout>): readonly string[] {
47
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- the list is a function of the layout to its callers, though no name in it is configurable today
48
+ export function requiredFiles(_layout: Required<KbLayout>): readonly string[] {
40
49
  return [
41
50
  'access.md',
42
- // The managed agent guide, under whatever this deployment calls it. The
43
- // PACKAGED template still carries it as `AGENTS.md` — one file, one
44
- // spelling in the tarball — so the write target and the template source
45
- // part company here and nowhere else (see {@link templateNameOf}).
46
- layout.agentsFile,
47
51
  '.bevelignore',
48
52
  '.gitignore',
49
53
  // The deployment preamble every connected agent is told at session start
@@ -54,33 +58,6 @@ export function requiredFiles(layout: Required<KbLayout>): readonly string[] {
54
58
  ];
55
59
  }
56
60
 
57
- /**
58
- * The template's own name for a required file. The guide is the one file whose
59
- * name on disk is a deployment's choice while its name in the template is
60
- * fixed; everything else is spelled the same on both sides.
61
- */
62
- function templateNameOf(repoRel: string, agentsFile: string): string {
63
- return repoRel === agentsFile ? LEGACY_AGENTS_FILE : repoRel;
64
- }
65
-
66
- /**
67
- * The sentence the managed guide carries about itself, and the ONLY thing that
68
- * makes a root `AGENTS.md` provably the platform's rather than the customer's.
69
- *
70
- * Matched on this one phrase rather than on the whole header: the lines around
71
- * it have changed between releases (they name the configured file now), and a
72
- * knowledge base seeded by any of those releases is still ours to remove. A
73
- * customer file would have to quote the platform's own claim about itself
74
- * verbatim to be mistaken for one, and the consequence of the mistake is a
75
- * deletion — which is why nothing looser will do.
76
- */
77
- const MANAGED_GUIDE_MARKER = '**This file is managed by the platform.**';
78
-
79
- /** Whether `text` is a copy of the platform's managed guide, of any vintage. */
80
- export function isManagedGuide(text: string): boolean {
81
- return text.includes(MANAGED_GUIDE_MARKER);
82
- }
83
-
84
61
  /**
85
62
  * Repo-root files the startup phase GENERATES rather than copies — today just
86
63
  * `roles.yaml`, rendered from `ADMIN_EMAIL` (see roles-yaml.step.ts and
@@ -161,13 +138,16 @@ export function reservedRootDirs(extraRootDirs: readonly string[], layout: Requi
161
138
 
162
139
  /**
163
140
  * The template top-up as an {@link OnServerStart} step: add any missing base
164
- * scaffolding to every PROTECTED branch, and keep the managed agent guide
165
- * current — under whatever this deployment calls it, and taking its own
166
- * stale `AGENTS.md` with it when that name was handed back to the customer. Drafts are deliberately out of scope — whatever the protected
167
- * branches gain, drafts fork from; a scaffolding addition on a draft would
168
- * surface as noise in its change request's diff. (Unlike the Groups→Plugins
169
- * rename, a missing file diffs as one file, not the whole tree — so the
170
- * uniform-application argument does not bite here.)
141
+ * scaffolding to every PROTECTED branch, and take the agent guide copies
142
+ * earlier releases wrote OUT of them — the guide is served from code now, and
143
+ * a copy left on disk would be read as the organisation's own conventions
144
+ * file, stale and under a header that says the platform owns it. Drafts are
145
+ * deliberately out of scope — whatever the protected branches gain, drafts
146
+ * fork from; a scaffolding addition on a draft would surface as noise in its
147
+ * change request's diff. (Unlike the Groups→Plugins rename, a missing file
148
+ * diffs as one file, not the whole tree — so the uniform-application argument
149
+ * does not bite here. A stale guide copy on a draft is recognised by its
150
+ * header and never served, see `modules/agent-guide`.)
171
151
  *
172
152
  * Everything is DECLARED on the branch handle; reads go against the pre-step
173
153
  * tree via `repoDir()`. Fail-open behavior from the lazy top-up (best-effort,
@@ -185,12 +165,6 @@ export class TemplateFilesStep implements OnServerStart {
185
165
  * claim a root without also shipping a template entry
186
166
  * for it.
187
167
  */
188
- /**
189
- * @param agentsFileLink Whether to keep the platform's pointer sentence in a
190
- * customer-owned root `AGENTS.md` — a GETTER, because
191
- * the setting behind it may be saved by the very
192
- * first-run save that then runs this phase.
193
- */
194
168
  constructor(
195
169
  private readonly disk: IFsProbe,
196
170
  /**
@@ -200,7 +174,6 @@ export class TemplateFilesStep implements OnServerStart {
200
174
  */
201
175
  private readonly kb: Pick<KbContext, 'layout'>,
202
176
  private readonly extraRootDirs: readonly string[] = [],
203
- private readonly agentsFileLink: () => boolean = () => true,
204
177
  ) {
205
178
  // Validated NOW, so a bad extra fails at boot beside the rest of the
206
179
  // wiring — but the list itself is NOT kept, for the reason `kb` says.
@@ -224,8 +197,6 @@ export class TemplateFilesStep implements OnServerStart {
224
197
  // Read ONCE per branch: a value re-read between the write and the ignore
225
198
  // rule could disagree with itself.
226
199
  const layout = this.kb.layout;
227
- const agentsFile = layout.agentsFile;
228
- const renamed = agentsFile !== LEGACY_AGENTS_FILE;
229
200
 
230
201
  for (const rel of requiredFiles(layout)) {
231
202
  // `lstat`, not `exists`: a DIRECTORY or SYMLINK squatting a required
@@ -242,58 +213,45 @@ export class TemplateFilesStep implements OnServerStart {
242
213
  'Remove or rename it — the platform requires this name to be a readable file.',
243
214
  );
244
215
  }
245
- let content = await templates.read(templateNameOf(rel, agentsFile));
216
+ let content = await templates.read(rel);
246
217
  // The on-disk merge below only runs against an EXISTING ignore file; a
247
- // freshly-declared one was merely assumed to carry the guide's rule —
248
- // true of the packaged template, not necessarily of a distribution's
249
- // custom one. Make it true here, so the managed conventions doc is
250
- // hidden from the file tree from the first boot either way. The same is
251
- // true of the deployment preamble: it is edited through External agent
252
- // access, not as an ordinary knowledge-base document.
253
- // …and a template still shipping the skills rule an earlier release
254
- // had (a distribution's copy, a stale packaged one) must not declare
255
- // it: the on-disk reconciliation below never sees a file that was
256
- // absent, so the declared content is reconciled here instead.
218
+ // freshly-declared one is reconciled here instead, so a distribution's
219
+ // custom template that predates a rule — or still ships one an earlier
220
+ // release had — declares the same file the merge would have produced.
221
+ // The deployment preamble is edited through External agent access, not
222
+ // as an ordinary knowledge-base document, so its rule is guaranteed;
223
+ // the rules that hid the skills root, the plugins root and the guide
224
+ // copies the platform used to write are taken out (see the merge below
225
+ // for why each).
257
226
  if (rel === IGNORE_FILENAME) {
258
227
  // A template still shipping the unanchored preamble rule an earlier
259
228
  // release had is respelled first, so the guarantee below adds nothing
260
229
  // beside it.
261
230
  content = withPlatformIgnorePatternRespelled(content, PREAMBLE_FILE, PREAMBLE_IGNORE_PATTERN);
262
- content = withoutIgnoreLine(
263
- withoutPlatformIgnorePattern(
264
- withIgnorePattern(
265
- withIgnorePattern(content, gitignoreLiteral(agentsFile), agentsFile),
266
- PREAMBLE_IGNORE_PATTERN,
267
- agentsFile,
231
+ content = withoutPlatformGuideRules(
232
+ withoutIgnoreLine(
233
+ withoutPlatformIgnorePattern(
234
+ withIgnorePattern(content, PREAMBLE_IGNORE_PATTERN),
235
+ `${layout.skillsDir}/`,
268
236
  ),
269
- `${layout.skillsDir}/`,
237
+ `${layout.pluginsDir}/`,
270
238
  ),
271
- `${layout.pluginsDir}/`,
239
+ [LEGACY_AGENTS_FILE, PRE_RENAME_AGENTS_FILE],
272
240
  );
273
- // …and a template (a distribution's, a stale packaged one) still
274
- // hiding `AGENTS.md` while this deployment's guide is called something
275
- // else would hide the CUSTOMER'S file from the first boot — the exact
276
- // thing the rename exists to prevent.
277
- if (renamed) content = withoutPlatformAgentsRule(content);
278
241
  }
279
242
  branch.write(rel, content);
280
243
  added.push(rel);
281
244
  }
282
245
 
283
- // The guide or mcp-description.md left VISIBLE by a stale `.bevelignore`
284
- // is closed here — and
285
- // UNCONDITIONALLY, not only when the file was just added: a KB whose
286
- // guide predates the CLAUDE.md→AGENTS.md rename has an ignore file
287
- // that lists the old name and knows nothing of the new one, so the
288
- // conventions doc shows up in the file tree and the agent view. A KB
289
- // whose deployment has just RENAMED the guide is the same story one
290
- // rename later: the rule names a file that is now the customer's.
246
+ // mcp-description.md left VISIBLE by a stale `.bevelignore` is closed
247
+ // here — and UNCONDITIONALLY, not only when the file was just added.
291
248
  // Idempotent: an ignore file already carrying the rule — or absent, in
292
249
  // which case the template's copy declared above arrives with the rule in
293
250
  // it — changes nothing and produces no note. Deliberately checked by
294
- // LINE PRESENCE, not effective outcome: a later `!<guide>` negation is
295
- // the operator explicitly choosing to SHOW the file, and hiding it is a
296
- // default this step provides, not a mandate it re-imposes every boot.
251
+ // LINE PRESENCE, not effective outcome: a later `!/mcp-description.md`
252
+ // negation is the operator explicitly choosing to SHOW the file, and
253
+ // hiding it is a default this step provides, not a mandate it re-imposes
254
+ // every boot.
297
255
  //
298
256
  // The shared-skills root goes the OTHER way. An earlier release hid it
299
257
  // like `Plugins/`; the Skills & Tools sidebar now renders it as a file
@@ -310,159 +268,104 @@ export class TemplateFilesStep implements OnServerStart {
310
268
  // it as a file tree too, so the rule that hid it since the first seed
311
269
  // comes out. That one has no comment to know it by — it was in the
312
270
  // template body from the start — so every line spelling it goes,
313
- // whoever wrote it (see `withoutIgnoreLine`). ONE read-modify-write for
314
- // all the rules: separate passes would each read the on-disk file and
315
- // a later declared write would lose an earlier one's.
271
+ // whoever wrote it (see `withoutIgnoreLine`).
272
+ //
273
+ // And the guide's rules go the same way as the skills root's. Every
274
+ // release that wrote the guide to disk hid it with a rule of its own —
275
+ // under `AGENTS.md`, under the name a deployment gave the guide (any
276
+ // name it ever gave it: the copies found at the root say which), and
277
+ // under `CLAUDE.md` for the copy that predates the rename. The guide is
278
+ // not on disk any more, so a root file under any of those names is the
279
+ // organisation's own conventions page, which they must be able to see
280
+ // and edit in the app. The platform's own lines come out, recognised by
281
+ // the comment or the template slot each release wrote them in (see
282
+ // {@link withoutPlatformGuideRules}); a rule an operator wrote by hand
283
+ // is theirs and stays. ONE read-modify-write for all the rules: separate
284
+ // passes would each read the on-disk file and a later declared write
285
+ // would lose an earlier one's.
286
+ //
287
+ // The copies come out first only so the one commit this step makes can
288
+ // name them in its subject; the rule pass below does not read the names
289
+ // — a guide rule is known by the platform's comment above it, whatever
290
+ // name it spells.
291
+ const retired = await this.retireGuideCopies(repoDir, branch);
292
+ added.push(...retired);
316
293
  added.push(
317
294
  ...(await this.reconcileIgnoreRules(repoDir, branch, {
318
295
  // The preamble rule is respelled before it is added: a knowledge base
319
296
  // that booted the release shipping the unanchored spelling carries the
320
297
  // platform's own line, and that line hides a nested namesake too.
321
298
  respell: [[PREAMBLE_FILE, PREAMBLE_IGNORE_PATTERN]],
322
- // As a gitignore PATTERN: a guide name that reads as syntax there is
323
- // escaped, or the rule would hide nothing.
324
- add: [gitignoreLiteral(agentsFile), PREAMBLE_IGNORE_PATTERN],
299
+ add: [PREAMBLE_IGNORE_PATTERN],
325
300
  drop: [`${this.kb.layout.skillsDir}/`],
326
301
  dropEvery: [`${this.kb.layout.pluginsDir}/`],
327
- // The guide's rule FOLLOWS its name. Once the guide is `HEXIS.md`, the
328
- // root's `AGENTS.md` is the customer's own conventions file and they
329
- // must be able to see and edit it in the app — so the platform's own
330
- // line comes out, recognised the way the skills-root line was
331
- // (see {@link withoutPlatformAgentsRule}); a rule an operator wrote by
332
- // hand is theirs and stays.
333
- dropAgentsRule: renamed,
302
+ // The two names the template itself shipped a rule for. A rule under
303
+ // any other name — a retired copy's, or a name no copy is left to
304
+ // tell — is known by the platform's comment above it instead.
305
+ guideNames: [LEGACY_AGENTS_FILE, PRE_RENAME_AGENTS_FILE],
334
306
  })),
335
307
  );
336
308
 
337
- // The guide is MANAGED, not merely seeded: the platform owns its content,
338
- // and a stale copy is replaced with the packaged template's every startup
339
- // phase. The file's own header says so, which is what makes overwriting
340
- // edits a stated contract instead of a surprise.
341
- let agentsRefreshed = false;
342
- if (
343
- !added.includes(agentsFile) &&
344
- (await templates.differsFrom(repoDir, agentsFile, LEGACY_AGENTS_FILE))
345
- ) {
346
- branch.write(agentsFile, await templates.read(LEGACY_AGENTS_FILE));
347
- added.push(agentsFile);
348
- agentsRefreshed = true;
349
- }
350
-
351
- // What becomes of the `AGENTS.md` the platform used to own, now that the
352
- // guide lives somewhere else. Nothing at all while the name is the default.
353
- if (renamed) {
354
- added.push(
355
- ...(await this.reconcileLegacyGuide(repoDir, branch, {
356
- agentsFile,
357
- // Only on the boot the rename lands — the boot that first writes the
358
- // guide under its new name. Said every boot after, the "kept" note
359
- // would caption commits about other things forever.
360
- announceKept: added.includes(agentsFile),
361
- })),
362
- );
363
- }
364
-
365
309
  added.push(...this.ensureRequiredDirs(repoDir, branch, await this.missingDirs(repoDir)));
366
310
 
367
311
  if (added.length === 0) return;
368
312
  // One honest line; it becomes the commit subject when this step is the
369
313
  // first to dirty the branch.
314
+ const others = added.filter((rel) => !retired.includes(rel));
370
315
  branch.note(
371
- agentsRefreshed && added.length === 1
372
- ? `Update ${agentsFile} to the current platform template`
316
+ retired.length > 0
317
+ ? `Remove the platform-written ${retired.join(' and ')} — the agent guide is served by the platform now` +
318
+ (others.length > 0 ? `; update ${others.join(', ')}` : '')
373
319
  : `Add missing KB scaffolding: ${added.join(', ')}`,
374
320
  );
375
321
  }
376
322
 
377
323
  /**
378
- * The root `AGENTS.md` on a deployment whose guide is called something else:
379
- * removed when the platform can PROVE it wrote it, otherwise left alone —
380
- * and, while the admin keeps the link setting on, given the one sentence
381
- * that points at the guide beside it.
382
- *
383
- * Removal is gated on the managed header and nothing else. An admin who
384
- * renames the guide on a knowledge base the platform seeded would otherwise
385
- * be left with stale platform content sitting under the very name they
386
- * wanted for their own file; an admin who renames it on a repository whose
387
- * `AGENTS.md` is their own must find that file byte-for-byte untouched. The
388
- * header is the one fact that tells the two apart, so it is the one thing
389
- * asked.
324
+ * The copies of the guide the platform wrote to the repository root while
325
+ * the guide was a file — under `AGENTS.md`, under `CLAUDE.md` from before
326
+ * the rename, and under every name a deployment ever gave the guide —
327
+ * removed when the platform can PROVE it wrote them, and otherwise left
328
+ * exactly alone.
390
329
  *
391
- * The pointer is bounded by three conditions, all of them the customer's to
392
- * control: the admin keeps the setting on, the file exists (one is NEVER
393
- * created for this), and the guide's name appears nowhere in the text. That
394
- * last one is a plain content search on purpose — a mention in the
395
- * customer's own words, a link, a heading, all count, and the platform stays
396
- * out of a file it does not own.
330
+ * Found by SCANNING the root's markdown files rather than by the names the
331
+ * deployment knows today: a deployment that renamed the guide more than
332
+ * once left a copy under each earlier name, and the current setting
333
+ * remembers only the last. The header is what makes a scan safe, and the
334
+ * one fact that tells a copy of ours from a file of theirs: the
335
+ * organisation's own `AGENTS.md`, a `CLAUDE.md` its people edited, a note
336
+ * of theirs that happens to sit at the root, must all be found byte for
337
+ * byte untouched. A SYMLINK or a directory is left as it is: reading a link
338
+ * follows it, so a link pointing at a copy of the guide — or at any other
339
+ * file carrying the header — would read as ours and the removal would take
340
+ * the organisation's entry. Links are never followed anywhere else in the
341
+ * platform, and they are not followed here.
397
342
  *
398
- * Returns the paths changed, for the note.
343
+ * Returns the names removed, in name order, for the note and for the
344
+ * ignore rules that hid them.
399
345
  */
400
- private async reconcileLegacyGuide(
401
- repoDir: string,
402
- branch: KbBranch,
403
- opts: { agentsFile: string; announceKept: boolean },
404
- ): Promise<string[]> {
405
- const legacyPath = path.join(repoDir, LEGACY_AGENTS_FILE);
406
- // `lstat` first, and a REGULAR FILE or nothing at all.
407
- //
408
- // Nothing there is the ordinary case: no customer file means nothing to
409
- // remove and nothing to point at the guide — the platform never creates an
410
- // `AGENTS.md` for this.
411
- //
412
- // Anything that is not a plain file is left exactly as it is. A SYMLINK is
413
- // the case that matters: reading one follows it, so a link pointing at a
414
- // knowledge base's managed guide — or at any other file carrying the
415
- // header — would read as "ours" and the removal would take the customer's
416
- // entry; writing one follows it too, and the pointer sentence would land
417
- // in a file at the other end that nobody asked us to edit. Links are never
418
- // followed anywhere else in the platform, and they are not followed here.
419
- // A directory under the name is the same answer for the same reason.
420
- const found = await this.disk.lstatOrNull(legacyPath);
421
- if (found === null || !found.isFile()) return [];
422
-
423
- let current: string;
424
- try {
425
- current = await this.disk.readTextFile(legacyPath);
426
- } catch (err) {
427
- // Gone between the probe and the read — a concurrent delete reads as the
428
- // absence it is, on the same terms as the probe above.
429
- if (isAbsence(err)) return [];
430
- throw err;
431
- }
432
-
433
- if (isManagedGuide(current)) {
434
- branch.remove(LEGACY_AGENTS_FILE);
435
- branch.note(`Remove the platform-written AGENTS.md — the agent guide is now ${opts.agentsFile}`);
436
- return [LEGACY_AGENTS_FILE];
437
- }
438
-
439
- if (opts.announceKept) {
440
- branch.note('Keep AGENTS.md — it is not a platform template, so it is the knowledge base\'s own');
441
- }
442
- // The admin's consent gates every write below, this file being theirs.
443
- if (!this.agentsFileLink()) return [];
444
-
445
- // A sentence of OURS already in the file is updated rather than joined by
446
- // a second one. The guide's name can be changed again, and after a second
447
- // rename the sentence this step wrote last time points at a file that is
448
- // no longer there — which asking "is the new name mentioned?" cannot see,
449
- // because the old sentence does not mention it.
450
- const retargeted = retargetAgentsFilePointer(current, opts.agentsFile);
451
- if (retargeted !== null) {
452
- if (retargeted === current) return [];
453
- branch.write(LEGACY_AGENTS_FILE, retargeted);
454
- branch.note(`Point the sentence in AGENTS.md at ${opts.agentsFile}`);
455
- return [LEGACY_AGENTS_FILE];
346
+ private async retireGuideCopies(repoDir: string, branch: KbBranch): Promise<string[]> {
347
+ const removed: string[] = [];
348
+ const entries = await fs.readdir(repoDir, { withFileTypes: true });
349
+ // `isFile` is false for a link, which is the point (see above).
350
+ const candidates = entries
351
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'))
352
+ .map((entry) => entry.name)
353
+ .sort();
354
+ for (const name of candidates) {
355
+ let current: string;
356
+ try {
357
+ current = await this.disk.readTextFile(path.join(repoDir, name));
358
+ } catch (err) {
359
+ // Gone between the listing and the read — a concurrent delete reads
360
+ // as the absence it is.
361
+ if (isAbsence(err)) continue;
362
+ throw err;
363
+ }
364
+ if (!isManagedGuide(current)) continue;
365
+ branch.remove(name);
366
+ removed.push(name);
456
367
  }
457
-
458
- // Nothing of ours in there, so the question is the customer's own text.
459
- // Asked through the shared reading, not a raw `includes`: the sentence
460
- // spells the name escaped and percent-encoded, so on a punctuated name the
461
- // copy written last boot need not carry the raw name at all.
462
- if (mentionsAgentsFile(current, opts.agentsFile)) return [];
463
- branch.write(LEGACY_AGENTS_FILE, withPointerSentence(current, opts.agentsFile));
464
- branch.note(`Add a pointer to ${opts.agentsFile} at the end of AGENTS.md`);
465
- return [LEGACY_AGENTS_FILE];
368
+ return removed;
466
369
  }
467
370
 
468
371
  /**
@@ -533,8 +436,8 @@ export class TemplateFilesStep implements OnServerStart {
533
436
  dropEvery?: string[];
534
437
  /** `[from, to]` pairs: a rule an earlier release wrote, and its spelling now. */
535
438
  respell?: ReadonlyArray<readonly [string, string]>;
536
- /** Take out the platform's own `AGENTS.md` line — the guide is called something else now. */
537
- dropAgentsRule?: boolean;
439
+ /** The names the template itself shipped a guide rule for; a rule under any other name is known by its comment (see {@link withoutPlatformGuideRules}). */
440
+ guideNames: readonly string[];
538
441
  },
539
442
  ): Promise<string[]> {
540
443
  let current: string;
@@ -553,15 +456,12 @@ export class TemplateFilesStep implements OnServerStart {
553
456
  (text, [from, to]) => withPlatformIgnorePatternRespelled(text, from, to),
554
457
  current,
555
458
  );
556
- const added = rules.add.reduce(
557
- (text, pattern) => withIgnorePattern(text, pattern, this.kb.layout.agentsFile),
558
- respelled,
559
- );
459
+ const added = rules.add.reduce((text, pattern) => withIgnorePattern(text, pattern), respelled);
560
460
  const dropped = (rules.dropEvery ?? []).reduce(
561
461
  (text, pattern) => withoutIgnoreLine(text, pattern),
562
462
  rules.drop.reduce((text, pattern) => withoutPlatformIgnorePattern(text, pattern), added),
563
463
  );
564
- const merged = rules.dropAgentsRule ? withoutPlatformAgentsRule(dropped) : dropped;
464
+ const merged = withoutPlatformGuideRules(dropped, rules.guideNames);
565
465
  if (merged === current) return [];
566
466
  branch.write(IGNORE_FILENAME, merged);
567
467
  return [IGNORE_FILENAME];
@@ -638,15 +538,102 @@ function followsTemplateHygieneBlock(kept: readonly string[]): boolean {
638
538
  return kept.slice(-block.length).every((line, i) => line.trim() === block[i]);
639
539
  }
640
540
 
541
+ /**
542
+ * `text` without the rules THE PLATFORM WROTE to hide the guide while it was a
543
+ * file. Under WHATEVER name: every rule sitting directly under one of the two
544
+ * comments the platform wrote above the guide's rule
545
+ * ({@link withoutPlatformConventionsRules}) — the name a deployment saved for
546
+ * the guide is not read any more, so the rule for it is known by its comment
547
+ * and by nothing else. Then the two names the template itself shipped a rule
548
+ * for, under each of `names`: the `AGENTS.md` line in the slot at the end of
549
+ * the template's own repo-hygiene block
550
+ * ({@link TEMPLATE_HYGIENE_BLOCK_ABOVE_AGENTS_RULE}) and the `CLAUDE.md` line
551
+ * under the two-line comment the template carried it with. Each goes with
552
+ * its comment.
553
+ *
554
+ * The guide is not on disk any more, which is what makes every one of these
555
+ * lines wrong: it hides a file the platform never writes, which is therefore
556
+ * the organisation's own. A `!AGENTS.md` negation is not the rule and stays,
557
+ * as everywhere else here, and so does a bare rule an operator wrote by hand.
558
+ */
559
+ export function withoutPlatformGuideRules(text: string, names: readonly string[]): string {
560
+ let out = withoutPlatformConventionsRules(text);
561
+ for (const name of new Set(names)) {
562
+ if (name === LEGACY_AGENTS_FILE) out = withoutPlatformAgentsRule(out);
563
+ else if (name === PRE_RENAME_AGENTS_FILE) out = withoutPlatformClaudeRule(out);
564
+ }
565
+ return out;
566
+ }
567
+
568
+ /**
569
+ * `text` without every rule line that sits DIRECTLY under one of the two
570
+ * comments the platform wrote above the guide's rule while the guide was a
571
+ * file ({@link PLATFORM_RULE_COMMENT}, {@link AGENTS_RULE_COMMENT}), whatever
572
+ * name the rule spells — `AGENTS.md`, `CLAUDE.md`, or the escaped form of a
573
+ * name a deployment chose — and without that comment, plus the blank line
574
+ * that opened the appended block. Those two comments were written above
575
+ * nothing else, so the comment is the whole provenance: a rule for a name
576
+ * nobody remembers is retired exactly like one for a name still known. A
577
+ * `!negation`, a blank or a further comment under the comment is not a rule
578
+ * and stays, comment included.
579
+ */
580
+ function withoutPlatformConventionsRules(text: string): string {
581
+ const lines = text.split('\n');
582
+ const kept: string[] = [];
583
+ for (const line of lines) {
584
+ const above = kept[kept.length - 1]?.trim();
585
+ const rule = line.trim();
586
+ const ours =
587
+ (above === PLATFORM_RULE_COMMENT || above === AGENTS_RULE_COMMENT) &&
588
+ rule !== '' &&
589
+ !rule.startsWith('#') &&
590
+ !rule.startsWith('!');
591
+ if (!ours) {
592
+ kept.push(line);
593
+ continue;
594
+ }
595
+ kept.pop();
596
+ if (kept.length > 1 && kept[kept.length - 1]?.trim() === '') kept.pop();
597
+ }
598
+ return kept.join('\n');
599
+ }
600
+
601
+ /** The two comment lines the packaged template carried above its `CLAUDE.md` rule, in order. */
602
+ const TEMPLATE_CLAUDE_RULE_COMMENT: readonly string[] = [
603
+ '# The pre-rename name. Listed so a knowledge base carrying both files hides',
604
+ '# both — top-up adds AGENTS.md but never deletes the CLAUDE.md beside it.',
605
+ ];
606
+
607
+ /**
608
+ * `text` without the `CLAUDE.md` line the packaged template wrote, and without
609
+ * the two comment lines it wrote above it. Provenance is that comment, as
610
+ * everywhere else here: a bare `CLAUDE.md` an operator wrote is theirs.
611
+ */
612
+ function withoutPlatformClaudeRule(text: string): string {
613
+ const lines = text.split('\n');
614
+ const kept: string[] = [];
615
+ for (const line of lines) {
616
+ const [first, second] = TEMPLATE_CLAUDE_RULE_COMMENT;
617
+ const ours =
618
+ line.trim() === PRE_RENAME_AGENTS_FILE &&
619
+ kept.length >= 2 &&
620
+ kept[kept.length - 1]!.trim() === second &&
621
+ kept[kept.length - 2]!.trim() === first;
622
+ if (!ours) {
623
+ kept.push(line);
624
+ continue;
625
+ }
626
+ kept.splice(-2, 2);
627
+ }
628
+ return kept.join('\n');
629
+ }
630
+
641
631
  /**
642
632
  * `text` without the `AGENTS.md` line THE PLATFORM WROTE — under its own
643
633
  * comment (either spelling), or in the slot at the end of the template's own
644
634
  * repo-hygiene block ({@link TEMPLATE_HYGIENE_BLOCK_ABOVE_AGENTS_RULE}) — and
645
- * without that comment.
646
- *
647
- * Called only when the guide has been renamed, which is what makes the line
648
- * wrong: it hides a file the platform no longer owns. A `!AGENTS.md` negation
649
- * is not the rule and stays, as everywhere else here.
635
+ * without that comment. A `!AGENTS.md` negation is not the rule and stays, as
636
+ * everywhere else here.
650
637
  */
651
638
  export function withoutPlatformAgentsRule(text: string): string {
652
639
  const lines = text.split('\n');
@@ -674,18 +661,6 @@ export function withoutPlatformAgentsRule(text: string): string {
674
661
  return kept.join('\n');
675
662
  }
676
663
 
677
- /**
678
- * `text` with the platform's pointer sentence appended as its own paragraph.
679
- *
680
- * One blank line between the customer's last line and ours, whether or not
681
- * their file ended in a newline: a sentence glued onto the end of their last
682
- * paragraph would read as a continuation of something they wrote.
683
- */
684
- export function withPointerSentence(text: string, agentsFile: string): string {
685
- const body = text.replace(/\n+$/, '');
686
- return `${body}\n\n${agentsFilePointerSentence(agentsFile)}\n`;
687
- }
688
-
689
664
  /** The comment written above the preamble rule on an existing knowledge base. */
690
665
  const PREAMBLE_RULE_COMMENT =
691
666
  '# Added by the platform: agent instructions are edited from External agent access.';
@@ -805,20 +780,13 @@ function withPlatformIgnorePatternRespelled(text: string, from: string, to: stri
805
780
  * still standing here is the operator's: it already hides the file, and
806
781
  * appending the anchored rule beside it would say nothing they have not.
807
782
  */
808
- function withIgnorePattern(text: string, pattern: string, agentsFile: string): string {
783
+ function withIgnorePattern(text: string, pattern: string): string {
809
784
  const lines = text.split('\n').map((l) => l.trim());
810
785
  const operatorPreambleRule =
811
786
  pattern === PREAMBLE_IGNORE_PATTERN &&
812
787
  (lines.includes(PREAMBLE_FILE) || lines.includes(`!${PREAMBLE_FILE}`));
813
788
  if (lines.includes(pattern) || lines.includes(`!${pattern}`) || operatorPreambleRule) return text;
814
789
  const separator = text.endsWith('\n') ? '' : '\n';
815
- // The guide's rule arrives as a gitignore literal (escaped where the name
816
- // needs it), so it is recognised in that spelling.
817
- const comment =
818
- pattern === PREAMBLE_IGNORE_PATTERN
819
- ? PREAMBLE_RULE_COMMENT
820
- : pattern === gitignoreLiteral(agentsFile)
821
- ? AGENTS_RULE_COMMENT
822
- : PLATFORM_RULE_COMMENT;
790
+ const comment = pattern === PREAMBLE_IGNORE_PATTERN ? PREAMBLE_RULE_COMMENT : PLATFORM_RULE_COMMENT;
823
791
  return `${text}${separator}\n${comment}\n${pattern}\n`;
824
792
  }