@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.
- package/agent-guide/access-control.md +234 -0
- package/agent-guide/conventions.md +27 -0
- package/agent-guide/directory-structure.md +145 -0
- package/agent-guide/finding-things.md +7 -0
- package/agent-guide/introduction.md +27 -0
- package/agent-guide/skills.md +47 -0
- package/agent-guide/tool-manuals.md +217 -0
- package/agent-guide/where-a-new-file-goes.md +36 -0
- package/dist/assets.d.ts +7 -0
- package/dist/assets.d.ts.map +1 -1
- package/dist/assets.js +9 -0
- package/dist/assets.js.map +1 -1
- package/dist/core/core-ports.d.ts +11 -0
- package/dist/core/core-ports.d.ts.map +1 -1
- package/dist/core/core-ports.js.map +1 -1
- package/dist/core/create-core-server.d.ts.map +1 -1
- package/dist/core/create-core-server.js +13 -2
- package/dist/core/create-core-server.js.map +1 -1
- package/dist/core/create-core-services.d.ts +9 -0
- package/dist/core/create-core-services.d.ts.map +1 -1
- package/dist/core/create-core-services.js +14 -4
- package/dist/core/create-core-services.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/access/access-control.interface.d.ts +9 -0
- package/dist/modules/access/access-control.interface.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.d.ts +1 -0
- package/dist/modules/access/access-control.service.d.ts.map +1 -1
- package/dist/modules/access/access-control.service.js +16 -0
- package/dist/modules/access/access-control.service.js.map +1 -1
- package/dist/modules/agent-guide/agent-guide.d.ts +139 -0
- package/dist/modules/agent-guide/agent-guide.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.js +191 -0
- package/dist/modules/agent-guide/agent-guide.js.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts +24 -0
- package/dist/modules/agent-guide/agent-guide.tools.d.ts.map +1 -0
- package/dist/modules/agent-guide/agent-guide.tools.js +100 -0
- package/dist/modules/agent-guide/agent-guide.tools.js.map +1 -0
- package/dist/modules/agent-guide/index.d.ts +4 -0
- package/dist/modules/agent-guide/index.d.ts.map +1 -0
- package/dist/modules/agent-guide/index.js +4 -0
- package/dist/modules/agent-guide/index.js.map +1 -0
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.d.ts.map +1 -1
- package/dist/modules/agent-instructions/agent-instructions.routes.js +3 -2
- package/dist/modules/agent-instructions/agent-instructions.routes.js.map +1 -1
- package/dist/modules/agent-instructions/compose.d.ts +9 -6
- package/dist/modules/agent-instructions/compose.d.ts.map +1 -1
- package/dist/modules/agent-instructions/compose.js +9 -6
- package/dist/modules/agent-instructions/compose.js.map +1 -1
- package/dist/modules/agent-instructions/index.d.ts +1 -1
- package/dist/modules/agent-instructions/index.d.ts.map +1 -1
- package/dist/modules/agent-instructions/index.js +1 -1
- package/dist/modules/agent-instructions/index.js.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.d.ts +10 -50
- package/dist/modules/agent-instructions/shared-file-rules.d.ts.map +1 -1
- package/dist/modules/agent-instructions/shared-file-rules.js +32 -85
- package/dist/modules/agent-instructions/shared-file-rules.js.map +1 -1
- package/dist/modules/mcp/mcp.service.d.ts +29 -2
- package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
- package/dist/modules/mcp/mcp.service.js +113 -16
- package/dist/modules/mcp/mcp.service.js.map +1 -1
- package/dist/modules/mcp/tool-schema-guard.d.ts +105 -0
- package/dist/modules/mcp/tool-schema-guard.d.ts.map +1 -0
- package/dist/modules/mcp/tool-schema-guard.js +171 -0
- package/dist/modules/mcp/tool-schema-guard.js.map +1 -0
- package/dist/modules/plugins/plugins.tools.d.ts +36 -2
- package/dist/modules/plugins/plugins.tools.d.ts.map +1 -1
- package/dist/modules/plugins/plugins.tools.js +71 -14
- package/dist/modules/plugins/plugins.tools.js.map +1 -1
- package/dist/modules/settings/deployment-settings.service.d.ts +0 -7
- package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
- package/dist/modules/settings/deployment-settings.service.js +14 -53
- package/dist/modules/settings/deployment-settings.service.js.map +1 -1
- package/dist/modules/settings/setup.routes.d.ts.map +1 -1
- package/dist/modules/settings/setup.routes.js +3 -6
- package/dist/modules/settings/setup.routes.js.map +1 -1
- package/dist/modules/skills/skills.tools.d.ts.map +1 -1
- package/dist/modules/skills/skills.tools.js +58 -16
- package/dist/modules/skills/skills.tools.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +23 -4
- package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.contract.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts +4 -0
- package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.service.js +14 -0
- package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts +7 -0
- package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
- package/dist/modules/tool-manuals/tool-manuals.tools.js +66 -36
- package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
- package/dist/modules/tool-registry/description-length.d.ts +14 -14
- package/dist/modules/tool-registry/description-length.d.ts.map +1 -1
- package/dist/modules/tool-registry/description-length.js +24 -26
- package/dist/modules/tool-registry/description-length.js.map +1 -1
- package/dist/modules/tool-registry/guide-first.d.ts +23 -0
- package/dist/modules/tool-registry/guide-first.d.ts.map +1 -0
- package/dist/modules/tool-registry/guide-first.js +32 -0
- package/dist/modules/tool-registry/guide-first.js.map +1 -0
- package/dist/modules/tool-registry/tool-registry.d.ts +6 -0
- package/dist/modules/tool-registry/tool-registry.d.ts.map +1 -1
- package/dist/modules/tool-registry/tool-registry.js +9 -2
- package/dist/modules/tool-registry/tool-registry.js.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts +449 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js +481 -0
- package/dist/modules/workflow/agent-tools/change-request-read-shape.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts +73 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.d.ts.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js +582 -0
- package/dist/modules/workflow/agent-tools/change-request-read.tools.js.map +1 -0
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts +12 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js +5 -1
- package/dist/modules/workflow/agent-tools/change-request-summary.js.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
- package/dist/modules/workflow/agent-tools/workflow.tools.js +9 -0
- package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
- package/dist/modules/workflow/git/git.service.d.ts +210 -13
- package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/git.service.js +456 -91
- package/dist/modules/workflow/git/git.service.js.map +1 -1
- package/dist/modules/workflow/git/merge-commit.d.ts +73 -0
- package/dist/modules/workflow/git/merge-commit.d.ts.map +1 -0
- package/dist/modules/workflow/git/merge-commit.js +89 -0
- package/dist/modules/workflow/git/merge-commit.js.map +1 -0
- package/dist/modules/workflow/git/pull-request.service.d.ts +94 -1
- package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
- package/dist/modules/workflow/git/pull-request.service.js +332 -37
- package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts +35 -0
- package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/review-workflow/review-workflow.service.js +178 -12
- package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
- package/dist/modules/workflow/workflow.routes.d.ts +6 -2
- package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.routes.js +7 -2
- package/dist/modules/workflow/workflow.routes.js.map +1 -1
- package/dist/modules/workflow/workflow.service.d.ts +4 -0
- package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
- package/dist/modules/workflow/workflow.service.js +3 -0
- package/dist/modules/workflow/workflow.service.js.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts +70 -0
- package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -1
- package/dist/modules/workspace/startup/kb-startup-runner.js +213 -20
- package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/seed-tree.js +22 -27
- package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts +58 -52
- package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-files.step.js +209 -223
- package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.d.ts +5 -3
- package/dist/modules/workspace/startup/steps/template-source.d.ts.map +1 -1
- package/dist/modules/workspace/startup/steps/template-source.js +5 -3
- package/dist/modules/workspace/startup/steps/template-source.js.map +1 -1
- package/dist/modules/workspace/workspace.tools.d.ts +10 -1
- package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
- package/dist/modules/workspace/workspace.tools.js +211 -18
- package/dist/modules/workspace/workspace.tools.js.map +1 -1
- package/dist/shared/domain-errors.d.ts +11 -0
- package/dist/shared/domain-errors.d.ts.map +1 -1
- package/dist/shared/domain-errors.js +14 -0
- package/dist/shared/domain-errors.js.map +1 -1
- package/dist/shared/hidden-tools.d.ts +44 -0
- package/dist/shared/hidden-tools.d.ts.map +1 -0
- package/dist/shared/hidden-tools.js +13 -0
- package/dist/shared/hidden-tools.js.map +1 -0
- package/kb-template/.bevelignore +0 -5
- package/package.json +4 -3
- package/src/__tests__/kb-layout-config.test.ts +10 -100
- package/src/__tests__/packaged-assets-ship.test.ts +54 -0
- package/src/assets.ts +10 -0
- package/src/core/core-ports.ts +11 -0
- package/src/core/create-core-server.ts +13 -2
- package/src/core/create-core-services.ts +28 -4
- package/src/index.ts +2 -2
- package/src/modules/access/__tests__/access-control.atref-batch.test.ts +58 -0
- package/src/modules/access/__tests__/access-control.platform-restore.test.ts +8 -7
- package/src/modules/access/__tests__/access-personal-plugin.test.ts +1 -18
- package/src/modules/access/access-control.interface.ts +15 -0
- package/src/modules/access/access-control.service.ts +21 -0
- package/src/modules/agent-guide/__tests__/agent-guide.test.ts +328 -0
- package/src/modules/agent-guide/__tests__/agent-guide.tools.test.ts +189 -0
- package/src/modules/agent-guide/agent-guide.tools.ts +122 -0
- package/src/modules/agent-guide/agent-guide.ts +291 -0
- package/src/modules/agent-guide/index.ts +21 -0
- package/src/modules/agent-instructions/__tests__/shared-file-rules.test.ts +28 -121
- package/src/modules/agent-instructions/agent-instructions.routes.ts +3 -2
- package/src/modules/agent-instructions/compose.ts +9 -6
- package/src/modules/agent-instructions/index.ts +0 -3
- package/src/modules/agent-instructions/shared-file-rules.ts +31 -93
- package/src/modules/mcp/__tests__/fake-downstream-mcp-server.ts +14 -3
- package/src/modules/mcp/__tests__/mcp.e2e.test.ts +250 -0
- package/src/modules/mcp/__tests__/mcp.service.test.ts +31 -23
- package/src/modules/mcp/__tests__/tool-schema-guard.test.ts +266 -0
- package/src/modules/mcp/mcp.service.ts +137 -19
- package/src/modules/mcp/tool-schema-guard.ts +196 -0
- package/src/modules/plugins/__tests__/plugins.tools.test.ts +154 -4
- package/src/modules/plugins/plugins.tools.ts +75 -15
- package/src/modules/settings/__tests__/deployment-settings.service.test.ts +26 -55
- package/src/modules/settings/deployment-settings.service.ts +13 -54
- package/src/modules/settings/setup.routes.ts +3 -6
- package/src/modules/skills/__tests__/skills.tools.description.test.ts +91 -0
- package/src/modules/skills/skills.tools.ts +62 -16
- package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +57 -0
- package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +73 -4
- package/src/modules/tool-manuals/tool-manuals.contract.ts +24 -4
- package/src/modules/tool-manuals/tool-manuals.service.ts +17 -0
- package/src/modules/tool-manuals/tool-manuals.tools.ts +74 -36
- package/src/modules/tool-registry/__tests__/own-tool-schemas.test.ts +160 -0
- package/src/modules/tool-registry/__tests__/tool-description-length.test.ts +61 -59
- package/src/modules/tool-registry/description-length.ts +24 -26
- package/src/modules/tool-registry/guide-first.ts +34 -0
- package/src/modules/tool-registry/tool-registry.ts +9 -2
- package/src/modules/workflow/__tests__/apply-failure.test.ts +6 -1
- package/src/modules/workflow/agent-tools/__tests__/change-request-read-shape.test.ts +705 -0
- package/src/modules/workflow/agent-tools/__tests__/change-request-read.tools.test.ts +1518 -0
- package/src/modules/workflow/agent-tools/__tests__/workflow.tools.test.ts +23 -2
- package/src/modules/workflow/agent-tools/change-request-read-shape.ts +712 -0
- package/src/modules/workflow/agent-tools/change-request-read.tools.ts +724 -0
- package/src/modules/workflow/agent-tools/change-request-summary.ts +5 -1
- package/src/modules/workflow/agent-tools/workflow.tools.ts +8 -0
- package/src/modules/workflow/git/__tests__/git.service.appliedChange.test.ts +285 -0
- package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +124 -0
- package/src/modules/workflow/git/__tests__/git.service.mergeChangeRequest.test.ts +334 -0
- package/src/modules/workflow/git/__tests__/pull-request.service.list-fetch.test.ts +72 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.placeholder.test.ts +24 -2
- package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +620 -1
- package/src/modules/workflow/git/git.service.ts +537 -94
- package/src/modules/workflow/git/merge-commit.ts +88 -0
- package/src/modules/workflow/git/pull-request.service.ts +380 -54
- package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +7 -1
- package/src/modules/workflow/review-workflow/__tests__/merge-records-own-commit.test.ts +407 -0
- package/src/modules/workflow/review-workflow/review-workflow.service.ts +189 -11
- package/src/modules/workflow/workflow.routes.ts +7 -2
- package/src/modules/workflow/workflow.service.ts +7 -0
- package/src/modules/workspace/__tests__/escape-sequences.routes.test.ts +4 -3
- package/src/modules/workspace/__tests__/workspace.routes.move-platform-files.test.ts +21 -10
- package/src/modules/workspace/__tests__/workspace.tools.agents-file.test.ts +33 -55
- package/src/modules/workspace/__tests__/workspace.tools.test.ts +255 -22
- package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +231 -1
- package/src/modules/workspace/startup/kb-startup-runner.ts +216 -19
- package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +191 -489
- package/src/modules/workspace/startup/steps/seed-tree.ts +21 -27
- package/src/modules/workspace/startup/steps/template-files.step.ts +217 -249
- package/src/modules/workspace/startup/steps/template-source.ts +5 -3
- package/src/modules/workspace/workspace.tools.ts +226 -16
- package/src/shared/domain-errors.ts +15 -0
- package/src/shared/hidden-tools.ts +45 -0
- package/kb-template/AGENTS.md +0 -730
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's agent guide: what every agent is told to read before its
|
|
3
|
+
* first read or change in a knowledge base — the layout, where a new file
|
|
4
|
+
* goes, the rules every file tool shares, access control, skills and tool
|
|
5
|
+
* manuals.
|
|
6
|
+
*
|
|
7
|
+
* It is TEXT THE CODE OWNS, not a file in the repository. It used to be
|
|
8
|
+
* written to every protected branch as `AGENTS.md` and refreshed on every
|
|
9
|
+
* start, which had three costs: a repository that already had an `AGENTS.md`
|
|
10
|
+
* of its own lost it or had to rename ours; a distribution that wanted to add
|
|
11
|
+
* to the guide had to fork the whole file and then drifted from every change
|
|
12
|
+
* made here; and the file sat in git history on every branch, hidden from the
|
|
13
|
+
* tree by an ignore rule it had to keep writing. Now the guide is composed
|
|
14
|
+
* from SECTIONS at the moment an agent asks for it — `get_agent_guide`, or a
|
|
15
|
+
* `read_file` of the guide's name at the repository root — and a distribution
|
|
16
|
+
* shapes it with a hook that sees the sections and returns the sections it
|
|
17
|
+
* wants (append, replace or drop by id), so what Hexis says reaches it without
|
|
18
|
+
* a copy to maintain.
|
|
19
|
+
*
|
|
20
|
+
* The sections ship as markdown files in the package's `agent-guide/` folder
|
|
21
|
+
* (see `assets.ts`), one per section, with the layout placeholders the old
|
|
22
|
+
* template carried (`{{knowledgeBaseDir}}`, `{{skillsDir}}`, `{{pluginsDir}}`,
|
|
23
|
+
* `{{agentsFile}}`). ONE section is computed rather than read: the rules every
|
|
24
|
+
* file tool shares, which the MCP handshake also sends — the same string in
|
|
25
|
+
* both places, from `shared-file-rules.ts`, so a rule cannot be changed in one
|
|
26
|
+
* and left stale in the other.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from 'node:fs/promises';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import {
|
|
32
|
+
|
|
33
|
+
renderKbLayoutPlaceholders,
|
|
34
|
+
resolveKbLayout,
|
|
35
|
+
type KbLayout,
|
|
36
|
+
} from '@bevel-software/platform-shared';
|
|
37
|
+
import { agentGuideDir } from '../../assets.js';
|
|
38
|
+
import { sharedFileRulesSection } from '../agent-instructions/shared-file-rules.js';
|
|
39
|
+
|
|
40
|
+
/** One section of the guide. */
|
|
41
|
+
export interface AgentGuideSection {
|
|
42
|
+
/**
|
|
43
|
+
* Stable id, so a distribution's hook can replace or drop a section by name
|
|
44
|
+
* and a test can say which one went missing. Core's ids are the
|
|
45
|
+
* {@link CORE_SECTION_IDS}.
|
|
46
|
+
*/
|
|
47
|
+
id: string;
|
|
48
|
+
/**
|
|
49
|
+
* The section's markdown, heading included (`## …`). May carry the layout
|
|
50
|
+
* placeholders; they are rendered with the names in effect when the guide is
|
|
51
|
+
* composed, so a section can name the knowledge folder without knowing what
|
|
52
|
+
* this deployment calls it.
|
|
53
|
+
*/
|
|
54
|
+
body: string;
|
|
55
|
+
/**
|
|
56
|
+
* The body is FINAL: already rendered, and not to be passed through the
|
|
57
|
+
* placeholder renderer. Set on the computed shared-rules section, which is
|
|
58
|
+
* rendered for the layout by the code that builds it; rendering it again
|
|
59
|
+
* could only misread a folder name it STATES as a placeholder.
|
|
60
|
+
*/
|
|
61
|
+
literal?: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A distribution's say over the guide. Called with core's sections, in order,
|
|
66
|
+
* and the layout in effect; returns the sections the guide is composed from.
|
|
67
|
+
* Append to the array to add sections, map over it to replace one by id,
|
|
68
|
+
* filter it to drop one. Called on every composition, so a hook that reads
|
|
69
|
+
* something live (a feature flag, a setting) is read each time.
|
|
70
|
+
*/
|
|
71
|
+
export type AgentGuideHook = (
|
|
72
|
+
sections: readonly AgentGuideSection[],
|
|
73
|
+
layout: Required<KbLayout>,
|
|
74
|
+
) => readonly AgentGuideSection[] | Promise<readonly AgentGuideSection[]>;
|
|
75
|
+
|
|
76
|
+
/** Composes the guide for the layout in effect — the shape every consumer reads. */
|
|
77
|
+
export type AgentGuideReader = () => Promise<string>;
|
|
78
|
+
|
|
79
|
+
/** The computed section: the rules every file tool shares. */
|
|
80
|
+
export const WORKING_WITH_FILES_SECTION_ID = 'working-with-files';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Core's sections, in reading order. Every id but the shared-rules one names
|
|
84
|
+
* a file in the package's `agent-guide/` folder.
|
|
85
|
+
*/
|
|
86
|
+
export const CORE_SECTION_IDS: readonly string[] = Object.freeze([
|
|
87
|
+
'introduction',
|
|
88
|
+
'directory-structure',
|
|
89
|
+
'where-a-new-file-goes',
|
|
90
|
+
WORKING_WITH_FILES_SECTION_ID,
|
|
91
|
+
'access-control',
|
|
92
|
+
'skills',
|
|
93
|
+
'tool-manuals',
|
|
94
|
+
'conventions',
|
|
95
|
+
'finding-things',
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The one line that proves a root guide file is the platform's and not the
|
|
100
|
+
* customer's: the blockquote the packaged guide opened with for as long as
|
|
101
|
+
* the guide was a file, under every release. Asked for WHERE the header put
|
|
102
|
+
* it — a blockquote line among the first lines of the file — and not anywhere
|
|
103
|
+
* in the text: a note of the organisation's own that quotes the platform's
|
|
104
|
+
* sentence in its body must not read as ours, because the consequence of the
|
|
105
|
+
* mistake is a deletion. Still read on every start, to take the copies
|
|
106
|
+
* earlier releases wrote out of the repository (see template-files.step.ts),
|
|
107
|
+
* and on every read of the guide's name, so a copy still on a draft is not
|
|
108
|
+
* served twice.
|
|
109
|
+
*/
|
|
110
|
+
const MANAGED_GUIDE_HEADER_LINE = '> **This file is managed by the platform.**';
|
|
111
|
+
|
|
112
|
+
/** How far down a file the header's blockquote can sit: under the title and a sentence or two, never further. */
|
|
113
|
+
const MANAGED_GUIDE_HEADER_WITHIN_LINES = 12;
|
|
114
|
+
|
|
115
|
+
/** Whether `text` is a copy of the guide a release wrote to disk, of any vintage. */
|
|
116
|
+
export function isManagedGuide(text: string): boolean {
|
|
117
|
+
return text
|
|
118
|
+
.split('\n', MANAGED_GUIDE_HEADER_WITHIN_LINES)
|
|
119
|
+
.some((line) => line.trimStart().startsWith(MANAGED_GUIDE_HEADER_LINE));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The raw section files, read once per process: the package does not change while it runs. */
|
|
123
|
+
let coreSectionFiles: Promise<ReadonlyMap<string, string>> | null = null;
|
|
124
|
+
|
|
125
|
+
async function readCoreSectionFiles(): Promise<ReadonlyMap<string, string>> {
|
|
126
|
+
const dir = agentGuideDir();
|
|
127
|
+
const entries = await Promise.all(
|
|
128
|
+
CORE_SECTION_IDS.filter((id) => id !== WORKING_WITH_FILES_SECTION_ID).map(
|
|
129
|
+
// Line endings normalised: a checkout on Windows may carry CRLF, and the
|
|
130
|
+
// guide is one text with one ending wherever it is served from.
|
|
131
|
+
async (id) => [id, (await fs.readFile(path.join(dir, `${id}.md`), 'utf8')).replace(/\r\n?/g, '\n')] as const,
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
return new Map(entries);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Core's sections for `layout`: the files, placeholders unrendered, with the
|
|
139
|
+
* shared-rules section computed and in its place. What a distribution's hook
|
|
140
|
+
* is handed.
|
|
141
|
+
*/
|
|
142
|
+
export async function coreAgentGuideSections(layout: KbLayout): Promise<AgentGuideSection[]> {
|
|
143
|
+
coreSectionFiles ??= readCoreSectionFiles();
|
|
144
|
+
let files: ReadonlyMap<string, string>;
|
|
145
|
+
try {
|
|
146
|
+
files = await coreSectionFiles;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
// A failed read is not cached: the next composition tries the disk again.
|
|
149
|
+
coreSectionFiles = null;
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
return CORE_SECTION_IDS.map((id) =>
|
|
153
|
+
id === WORKING_WITH_FILES_SECTION_ID
|
|
154
|
+
? { id, body: sharedFileRulesSection(layout), literal: true }
|
|
155
|
+
: { id, body: files.get(id)! },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What the guide is composed for, beyond the layout. */
|
|
160
|
+
export interface AgentGuideContext {
|
|
161
|
+
/**
|
|
162
|
+
* The name of the repository's checkout folder inside a workspace, which
|
|
163
|
+
* the file tools take paths under (`knowledge-base/` by default). Rendered
|
|
164
|
+
* into `{{kbDirName}}` so the paths the guide shows are the paths this
|
|
165
|
+
* deployment's tools report back.
|
|
166
|
+
*/
|
|
167
|
+
kbDirName?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The checkout folder's name when none is given — core's own default. */
|
|
171
|
+
const DEFAULT_KB_DIR_NAME = 'knowledge-base';
|
|
172
|
+
|
|
173
|
+
/** One section of the guide as an agent reads it: rendered, with the heading's text as its title. */
|
|
174
|
+
export interface RenderedGuideSection {
|
|
175
|
+
id: string;
|
|
176
|
+
/** The heading line's text, without its `#`s — what the tool lists the section as. */
|
|
177
|
+
title: string;
|
|
178
|
+
/** The section's markdown, heading included, rendered for the layout. */
|
|
179
|
+
body: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Composes the guide's sections for the layout in effect — what the guide tool reads. */
|
|
183
|
+
export type AgentGuideSectionsReader = () => Promise<RenderedGuideSection[]>;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The guide's sections as an agent reads them: core's sections through the
|
|
187
|
+
* distribution's hook (when there is one), rendered for the layout, each
|
|
188
|
+
* with its title. The whole guide is these joined (see
|
|
189
|
+
* {@link composeAgentGuide}); `get_agent_guide` also serves one at a time.
|
|
190
|
+
*
|
|
191
|
+
* A hook may append, replace and drop sections, with ONE exception: the
|
|
192
|
+
* shared file rules ({@link WORKING_WITH_FILES_SECTION_ID}) must come back
|
|
193
|
+
* WITH TEXT IN THEM, as core's or as the hook's own replacement. A client
|
|
194
|
+
* that drops the handshake instructions has the guide as the only place to
|
|
195
|
+
* read the rules — a guide without them would leave agents nothing to read.
|
|
196
|
+
* A hook that drops the section, or hands it back empty, is a composition
|
|
197
|
+
* error, thrown rather than served. Judged on what would be SERVED — the
|
|
198
|
+
* rendered, trimmed body — so a replacement of whitespace is caught like a
|
|
199
|
+
* missing one, rather than passing the check and vanishing with the other
|
|
200
|
+
* empty sections below.
|
|
201
|
+
*/
|
|
202
|
+
export async function agentGuideSections(
|
|
203
|
+
layout: KbLayout,
|
|
204
|
+
hook?: AgentGuideHook,
|
|
205
|
+
context: AgentGuideContext = {},
|
|
206
|
+
): Promise<RenderedGuideSection[]> {
|
|
207
|
+
const resolved = resolveKbLayout(layout);
|
|
208
|
+
const core = await coreAgentGuideSections(resolved);
|
|
209
|
+
const sections = hook ? await hook(core, resolved) : core;
|
|
210
|
+
const kbDirName = context.kbDirName?.trim() || DEFAULT_KB_DIR_NAME;
|
|
211
|
+
const rendered = sections.map((section) => {
|
|
212
|
+
const body = (
|
|
213
|
+
section.literal
|
|
214
|
+
? section.body
|
|
215
|
+
: renderKbLayoutPlaceholders(section.body.replaceAll('{{kbDirName}}', () => kbDirName), resolved)
|
|
216
|
+
).trim();
|
|
217
|
+
return { id: section.id, title: titleOf(body, section.id), body };
|
|
218
|
+
});
|
|
219
|
+
// Judged over EVERY section under the id — a hook may return the id twice
|
|
220
|
+
// — on what survives the empty-body filter below: one with text is enough.
|
|
221
|
+
const rules = rendered.filter((section) => section.id === WORKING_WITH_FILES_SECTION_ID);
|
|
222
|
+
if (!rules.some((section) => section.body.length > 0)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`The agent guide hook ${rules.length === 0 ? 'dropped' : 'emptied'} the "${WORKING_WITH_FILES_SECTION_ID}" ` +
|
|
225
|
+
'section, which every file tool points at. Keep it, or replace it under the same id with the rules in it.',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return rendered.filter((section) => section.body.length > 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The heading's text, or the id when the section opens with no heading. An
|
|
233
|
+
* ATX heading may close with a run of `#` of its own (`## Custom ##`); that
|
|
234
|
+
* is a delimiter, not part of the title.
|
|
235
|
+
*/
|
|
236
|
+
function titleOf(body: string, id: string): string {
|
|
237
|
+
const first = body.split('\n', 1)[0] ?? '';
|
|
238
|
+
const heading = /^#{1,6}\s+(.*?)(?:\s+#+)?\s*$/.exec(first);
|
|
239
|
+
return heading ? heading[1]!.trim() : id;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The guide as an agent reads it whole: the sections joined by blank lines,
|
|
244
|
+
* ending in one newline.
|
|
245
|
+
*/
|
|
246
|
+
export async function composeAgentGuide(
|
|
247
|
+
layout: KbLayout,
|
|
248
|
+
hook?: AgentGuideHook,
|
|
249
|
+
context: AgentGuideContext = {},
|
|
250
|
+
): Promise<string> {
|
|
251
|
+
return joinGuideSections(await agentGuideSections(layout, hook, context));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The sections as one document, the way `composeAgentGuide` joins them. */
|
|
255
|
+
export function joinGuideSections(sections: readonly RenderedGuideSection[]): string {
|
|
256
|
+
return `${sections.map((section) => section.body).join('\n\n')}\n`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* What a `read_file` of the guide's name answers when the knowledge base has an
|
|
261
|
+
* `AGENTS.md` of its own: their text first, whole, then a rule and one line
|
|
262
|
+
* saying what follows, then the platform's guide. Theirs first because it is
|
|
263
|
+
* the more specific of the two, and the line between so neither is read as
|
|
264
|
+
* part of the other.
|
|
265
|
+
*/
|
|
266
|
+
export const PLATFORM_GUIDE_SEPARATOR =
|
|
267
|
+
"---\n\n_The text above is this knowledge base's own conventions file. The platform's guide follows; `get_agent_guide` returns it on its own._";
|
|
268
|
+
|
|
269
|
+
export function withPlatformGuideAppended(ownText: string, guide: string): string {
|
|
270
|
+
// Their text as they wrote it, line endings normalised and the trailing
|
|
271
|
+
// newlines folded into the one blank line before the separator. Nothing
|
|
272
|
+
// else is touched: leading indentation and trailing spaces are markdown.
|
|
273
|
+
const own = ownText.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
|
|
274
|
+
// A file with nothing in it has nothing to put first, and a separator
|
|
275
|
+
// above nothing would claim a conventions file that says nothing.
|
|
276
|
+
if (own.trim().length === 0) return guide;
|
|
277
|
+
return `${own}\n\n${PLATFORM_GUIDE_SEPARATOR}\n\n${guide}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** The one name the guide is read by: `AGENTS.md` at the repository root, the name coding agents look for. */
|
|
281
|
+
export const AGENT_GUIDE_FILE = 'AGENTS.md';
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Whether a repository-relative path is where the guide is read — `AGENTS.md`
|
|
285
|
+
* at the repository root, exactly spelled, like every platform path. One
|
|
286
|
+
* name on every deployment: there is no setting for it any more.
|
|
287
|
+
*/
|
|
288
|
+
export function isAgentGuidePath(repoRelativePath: string): boolean {
|
|
289
|
+
const norm = repoRelativePath.replace(/^\.?\/+/, '').replace(/\/+$/, '');
|
|
290
|
+
return norm === AGENT_GUIDE_FILE;
|
|
291
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AGENT_GUIDE_FILE,
|
|
3
|
+
CORE_SECTION_IDS,
|
|
4
|
+
PLATFORM_GUIDE_SEPARATOR,
|
|
5
|
+
WORKING_WITH_FILES_SECTION_ID,
|
|
6
|
+
agentGuideSections,
|
|
7
|
+
composeAgentGuide,
|
|
8
|
+
coreAgentGuideSections,
|
|
9
|
+
isAgentGuidePath,
|
|
10
|
+
isManagedGuide,
|
|
11
|
+
joinGuideSections,
|
|
12
|
+
withPlatformGuideAppended,
|
|
13
|
+
type AgentGuideContext,
|
|
14
|
+
type AgentGuideHook,
|
|
15
|
+
type AgentGuideReader,
|
|
16
|
+
type AgentGuideSection,
|
|
17
|
+
type AgentGuideSectionsReader,
|
|
18
|
+
type RenderedGuideSection,
|
|
19
|
+
} from './agent-guide.js';
|
|
20
|
+
export { GET_AGENT_GUIDE_TOOL, registerAgentGuideTool } from './agent-guide.tools.js';
|
|
21
|
+
export { GUIDE_FIRST_SENTENCE, withGuideFirst } from '../tool-registry/guide-first.js';
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_KB_LAYOUT,
|
|
5
|
-
LEGACY_AGENTS_FILE,
|
|
6
|
-
PREAMBLE_CAP,
|
|
7
|
-
renderKbLayoutPlaceholders,
|
|
8
|
-
} from '@bevel-software/platform-shared';
|
|
1
|
+
import { DEFAULT_KB_LAYOUT, PREAMBLE_CAP } from '@bevel-software/platform-shared';
|
|
9
2
|
import { describe, expect, it } from 'vitest';
|
|
10
|
-
import {
|
|
11
|
-
import { renderTemplateText, SHARED_FILE_RULES_PLACEHOLDER } from '../../workspace/startup/steps/template-source.js';
|
|
3
|
+
import { composeAgentGuide } from '../../agent-guide/agent-guide.js';
|
|
12
4
|
import {
|
|
13
5
|
INSTRUCTIONS_CAP,
|
|
14
6
|
PLATFORM_HEADER,
|
|
@@ -17,37 +9,29 @@ import {
|
|
|
17
9
|
platformInstructions,
|
|
18
10
|
} from '../compose.js';
|
|
19
11
|
import {
|
|
20
|
-
POINTER_GUIDE_NAME_BUDGET,
|
|
21
12
|
SHARED_FILE_RULES_CAP,
|
|
22
|
-
SHARED_RULES_POINTER_MAX,
|
|
23
13
|
SHARED_RULES_SECTION,
|
|
24
14
|
sharedFileRules,
|
|
25
15
|
sharedFileRulesSection,
|
|
26
|
-
sharedRulesPointer,
|
|
27
16
|
} from '../shared-file-rules.js';
|
|
28
17
|
|
|
29
18
|
/**
|
|
30
19
|
* The rules every file tool shares are stated in TWO places — the handshake
|
|
31
|
-
* `instructions` and the platform
|
|
20
|
+
* `instructions` and the platform's agent guide — and in neither tool
|
|
32
21
|
* description. These tests are what makes "stated once" true: the two places
|
|
33
22
|
* carry the SAME string, built from one text, so a rule cannot be changed in one
|
|
34
23
|
* and left stale in the other.
|
|
35
24
|
*/
|
|
36
25
|
|
|
37
|
-
/** The packaged template's agent guide, as it ships (placeholders unrendered). */
|
|
38
|
-
async function guideTemplate(): Promise<string> {
|
|
39
|
-
return readFile(path.join(defaultKbTemplateDir(), LEGACY_AGENTS_FILE), 'utf8');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
26
|
describe('the shared file rules are one text, in two places', () => {
|
|
43
|
-
it('puts the identical section in the handshake instructions and in the
|
|
27
|
+
it('puts the identical section in the handshake instructions and in the agent guide', async () => {
|
|
44
28
|
const section = sharedFileRulesSection(DEFAULT_KB_LAYOUT);
|
|
45
29
|
expect(section.startsWith(`## ${SHARED_RULES_SECTION}\n\n`)).toBe(true);
|
|
46
30
|
|
|
47
31
|
const instructions = composeAgentInstructions(null).instructions;
|
|
48
32
|
expect(instructions).toContain(section);
|
|
49
33
|
|
|
50
|
-
const guide =
|
|
34
|
+
const guide = await composeAgentGuide(DEFAULT_KB_LAYOUT);
|
|
51
35
|
expect(guide).toContain(section);
|
|
52
36
|
|
|
53
37
|
// Byte-for-byte the same text in both, which is the whole point: neither is
|
|
@@ -62,7 +46,7 @@ describe('the shared file rules are one text, in two places', () => {
|
|
|
62
46
|
|
|
63
47
|
it('states every rule in both places, and the content rule exactly once in each', async () => {
|
|
64
48
|
const instructions = composeAgentInstructions(null).instructions;
|
|
65
|
-
const guide =
|
|
49
|
+
const guide = await composeAgentGuide(DEFAULT_KB_LAYOUT);
|
|
66
50
|
const rules = sharedFileRules(DEFAULT_KB_LAYOUT);
|
|
67
51
|
expect(rules.map((r) => r.id)).toEqual([
|
|
68
52
|
'agent-guide',
|
|
@@ -87,7 +71,7 @@ describe('the shared file rules are one text, in two places', () => {
|
|
|
87
71
|
});
|
|
88
72
|
|
|
89
73
|
it('carries the whole content rule — the refused families, the byte tools and the upload path', async () => {
|
|
90
|
-
for (const text of [composeAgentInstructions(null).instructions,
|
|
74
|
+
for (const text of [composeAgentInstructions(null).instructions, await composeAgentGuide(DEFAULT_KB_LAYOUT)]) {
|
|
91
75
|
expect(text).toContain('`binary_not_writable`');
|
|
92
76
|
expect(text).toContain('copy_file, move_file and delete_file act on bytes of any kind');
|
|
93
77
|
expect(text).toContain('unzip extracts the entries of a `.zip`');
|
|
@@ -102,109 +86,32 @@ describe('the shared file rules are one text, in two places', () => {
|
|
|
102
86
|
}
|
|
103
87
|
});
|
|
104
88
|
|
|
105
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The guide is no file: it is reached by one name at the KB root and by one
|
|
91
|
+
* tool, on every deployment. The first rule says so, and names the
|
|
92
|
+
* organisation's own conventions file beside it — which is what a
|
|
93
|
+
* `read_file` of that name serves first.
|
|
94
|
+
*/
|
|
95
|
+
it('tells an agent to call get_agent_guide first, and that AGENTS.md answers with the same guide after the organisation\'s own', () => {
|
|
96
|
+
const rule = sharedFileRules(DEFAULT_KB_LAYOUT).find((r) => r.id === 'agent-guide')!.body;
|
|
97
|
+
expect(rule).toContain("call `get_agent_guide` and read the platform's guide");
|
|
98
|
+
expect(rule).toContain('read_file on `AGENTS.md` at the KB root answers with the same guide');
|
|
99
|
+
expect(rule).toContain("after the organisation's own conventions file of that name");
|
|
100
|
+
// The pre-rename name is still offered, for a knowledge base that kept one.
|
|
101
|
+
expect(rule).toContain('`CLAUDE.md`');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('names one guide on every deployment, whatever name a deployment once saved for the written one', () => {
|
|
106
105
|
const layout = { ...DEFAULT_KB_LAYOUT, agentsFile: 'HEXIS.md' };
|
|
107
106
|
const section = sharedFileRulesSection(layout);
|
|
108
|
-
|
|
109
|
-
expect(section).toContain('
|
|
110
|
-
expect(section).toContain('`CLAUDE.md` on a knowledge base seeded before it was renamed');
|
|
111
|
-
// The platform files a move refuses list the guide under its own name too.
|
|
112
|
-
expect(section).toContain('`HEXIS.md`');
|
|
107
|
+
expect(section).toBe(sharedFileRulesSection(DEFAULT_KB_LAYOUT));
|
|
108
|
+
expect(section).not.toContain('HEXIS.md');
|
|
113
109
|
expect(composeAgentInstructions(null, layout).instructions).toContain(section);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
it('keeps the pointer bounded whatever the guide is called', () => {
|
|
119
|
-
// The pointer rides on every file tool, so its length is not the guide's
|
|
120
|
-
// business: `validateFilename` allows a 255-byte name, and an unbounded
|
|
121
|
-
// pointer would reach 318 characters and push `file_stat` past the
|
|
122
|
-
// description cap — on a renamed deployment only, which no measurement
|
|
123
|
-
// taken under the default layout would ever show.
|
|
124
|
-
const nameOfLength = (n: number): string => `${'x'.repeat(n - 3)}.md`;
|
|
125
|
-
for (const length of [9, POINTER_GUIDE_NAME_BUDGET, POINTER_GUIDE_NAME_BUDGET + 1, 120, 255]) {
|
|
126
|
-
const pointer = sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: nameOfLength(length) });
|
|
127
|
-
expect(pointer.length, `a ${length}-character name`).toBeLessThanOrEqual(SHARED_RULES_POINTER_MAX);
|
|
128
|
-
}
|
|
129
|
-
// Up to the budget the file is NAMED, which is the better sentence.
|
|
130
|
-
const named = nameOfLength(POINTER_GUIDE_NAME_BUDGET);
|
|
131
|
-
expect(sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: named })).toContain(named);
|
|
132
|
-
// Past it the guide is named by its role instead — a sentence that still
|
|
133
|
-
// says where to look, rather than a catalog entry the client cuts.
|
|
134
|
-
const tooLong = nameOfLength(POINTER_GUIDE_NAME_BUDGET + 1);
|
|
135
|
-
const fallback = sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: tooLong });
|
|
136
|
-
expect(fallback).not.toContain(tooLong);
|
|
137
|
-
expect(fallback).toBe(` Shared rules for all file tools: see "${SHARED_RULES_SECTION}" in the agent guide at the KB root.`);
|
|
138
|
-
// Either way the section's first rule still names the file, so the name is
|
|
139
|
-
// never actually lost.
|
|
140
|
-
expect(sharedFileRulesSection({ ...DEFAULT_KB_LAYOUT, agentsFile: tooLong })).toContain(tooLong);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('points at the section with one short sentence under the default name', () => {
|
|
144
|
-
expect(sharedRulesPointer(DEFAULT_KB_LAYOUT)).toBe(
|
|
145
|
-
' Shared rules for all file tools: see "Working with files" in AGENTS.md.',
|
|
146
|
-
);
|
|
147
|
-
// An unset guide name means the default, as it does everywhere else.
|
|
148
|
-
expect(sharedRulesPointer({ ...DEFAULT_KB_LAYOUT, agentsFile: undefined })).toBe(
|
|
149
|
-
sharedRulesPointer(DEFAULT_KB_LAYOUT),
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe('the guide template asks for the rules rather than repeating them', () => {
|
|
155
|
-
it('carries the placeholder once, and no rule text of its own', async () => {
|
|
156
|
-
const template = await guideTemplate();
|
|
157
|
-
expect(template.split(SHARED_FILE_RULES_PLACEHOLDER)).toHaveLength(2);
|
|
158
|
-
// The rules are not ALSO written into the template — that is the copy that
|
|
159
|
-
// would drift. Checked on a fragment of each rule rather than on the whole.
|
|
160
|
-
for (const rule of sharedFileRules(DEFAULT_KB_LAYOUT)) {
|
|
161
|
-
expect(template, rule.id).not.toContain(rule.body.slice(0, 60));
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('renders the placeholder nowhere else, so other managed files are untouched', async () => {
|
|
166
|
-
const access = await readFile(path.join(defaultKbTemplateDir(), 'access.md'), 'utf8');
|
|
167
|
-
expect(access).not.toContain(SHARED_FILE_RULES_PLACEHOLDER);
|
|
168
|
-
// So rendering it gains no rule text: whatever the layout renderer does to
|
|
169
|
-
// its own tokens, the section is nowhere in the result.
|
|
170
|
-
const rendered = renderTemplateText(access, DEFAULT_KB_LAYOUT);
|
|
171
|
-
expect(rendered).not.toContain(sharedFileRulesSection(DEFAULT_KB_LAYOUT));
|
|
172
|
-
expect(rendered).not.toContain(`## ${SHARED_RULES_SECTION}`);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('leaves no placeholder behind once rendered', async () => {
|
|
176
|
-
const guide = renderTemplateText(await guideTemplate(), DEFAULT_KB_LAYOUT);
|
|
177
|
-
expect(guide).not.toContain('{{');
|
|
110
|
+
// The guide is not a platform file under any name: the list a move
|
|
111
|
+
// refuses does not carry it.
|
|
112
|
+
expect(section).toContain('`access.md` or `.bevelignore` in any folder, `roles.yaml` at the repository root');
|
|
178
113
|
});
|
|
179
114
|
|
|
180
|
-
it('renders the layout tokens first, then injects the rules — so the rules are never re-rendered', async () => {
|
|
181
|
-
// A layout the two orders DISAGREE on, which an ordinary one does not: the
|
|
182
|
-
// section states the knowledge-base folder's name, so a deployment whose
|
|
183
|
-
// folder is literally called `{{skillsDir}}` puts a layout token inside the
|
|
184
|
-
// rendered section. Injecting first would then rewrite it on the second
|
|
185
|
-
// pass and the guide would name the skills folder where the rule means the
|
|
186
|
-
// knowledge-base one. Pathological on purpose: it is the only kind of input
|
|
187
|
-
// on which the order is observable at all, which is why a test that pins
|
|
188
|
-
// the order has to use one.
|
|
189
|
-
const layout = { ...DEFAULT_KB_LAYOUT, knowledgeBaseDir: '{{skillsDir}}', skillsDir: 'Playbooks' };
|
|
190
|
-
const section = sharedFileRulesSection(layout);
|
|
191
|
-
expect(section).toContain('{{skillsDir}}/');
|
|
192
|
-
|
|
193
|
-
const template = `Skills live in {{skillsDir}}/.\n\n${SHARED_FILE_RULES_PLACEHOLDER}\n`;
|
|
194
|
-
const rendered = renderTemplateText(template, layout);
|
|
195
|
-
// The author's token is rendered, and the section survives byte for byte.
|
|
196
|
-
expect(rendered).toBe(`Skills live in Playbooks/.\n\n${section}\n`);
|
|
197
|
-
|
|
198
|
-
// And the other order really does differ on this input, so the assertion
|
|
199
|
-
// above is load-bearing rather than true of both.
|
|
200
|
-
const injectedFirst = renderKbLayoutPlaceholders(
|
|
201
|
-
template.replaceAll(SHARED_FILE_RULES_PLACEHOLDER, () => section),
|
|
202
|
-
layout,
|
|
203
|
-
);
|
|
204
|
-
expect(injectedFirst).not.toBe(rendered);
|
|
205
|
-
expect(injectedFirst).not.toContain(section);
|
|
206
|
-
expect(injectedFirst).toContain('`Playbooks/`) and git metadata are refused');
|
|
207
|
-
});
|
|
208
115
|
});
|
|
209
116
|
|
|
210
117
|
describe('the handshake text stays inside the length it pins', () => {
|
|
@@ -28,8 +28,9 @@ export function createAgentInstructionsRoutes(
|
|
|
28
28
|
readPreamble: AgentPreambleReader,
|
|
29
29
|
/**
|
|
30
30
|
* The layout in effect, read per request — the shared file rules name the
|
|
31
|
-
*
|
|
32
|
-
* Optional so a construction with no layout in
|
|
31
|
+
* guide by the name a deployment saved for it, which the setup save may
|
|
32
|
+
* change without a restart. Optional so a construction with no layout in
|
|
33
|
+
* hand gets `AGENTS.md`.
|
|
33
34
|
*/
|
|
34
35
|
kbLayout?: () => KbLayout,
|
|
35
36
|
): express.Router {
|
|
@@ -64,8 +64,10 @@ export const TOOL_PREFIX_LINE = "This organisation's knowledge base. Search it b
|
|
|
64
64
|
* `header` field carries and what the card shows as fixed and not editable;
|
|
65
65
|
* the admin's preamble follows it.
|
|
66
66
|
*
|
|
67
|
-
* A FUNCTION of the layout, because the shared rules name the
|
|
68
|
-
*
|
|
67
|
+
* A FUNCTION of the layout, because the shared rules name the platform files
|
|
68
|
+
* by the root names a deployment chose (`Skills/`, `Plugins/`), which are
|
|
69
|
+
* deployment settings. The guide's name is not one of them: it is `AGENTS.md`
|
|
70
|
+
* everywhere.
|
|
69
71
|
*/
|
|
70
72
|
export function platformInstructions(layout: KbLayout): string {
|
|
71
73
|
return `${PLATFORM_HEADER}\n\n${sharedFileRulesSection(layout)}`;
|
|
@@ -119,10 +121,11 @@ export interface ComposedAgentInstructions {
|
|
|
119
121
|
* unterminated `<!--` strips everything after it, so the most likely editing
|
|
120
122
|
* slip withholds text rather than leaking it.
|
|
121
123
|
*
|
|
122
|
-
* `layout` decides
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
124
|
+
* `layout` decides the root names the shared rules spell (`Skills/`,
|
|
125
|
+
* `Plugins/`, the knowledge folder) and defaults to the standard ones — a
|
|
126
|
+
* caller with no layout in hand (a test, a surface that predates the setting)
|
|
127
|
+
* gets the defaults. The guide's name is not among them: it is `AGENTS.md` on
|
|
128
|
+
* every deployment.
|
|
126
129
|
*/
|
|
127
130
|
export function composeAgentInstructions(
|
|
128
131
|
preamble: string | null,
|
|
@@ -13,13 +13,10 @@ export {
|
|
|
13
13
|
type ComposedAgentInstructions,
|
|
14
14
|
} from './compose.js';
|
|
15
15
|
export {
|
|
16
|
-
POINTER_GUIDE_NAME_BUDGET,
|
|
17
16
|
SHARED_FILE_RULES_CAP,
|
|
18
|
-
SHARED_RULES_POINTER_MAX,
|
|
19
17
|
SHARED_RULES_SECTION,
|
|
20
18
|
sharedFileRules,
|
|
21
19
|
sharedFileRulesSection,
|
|
22
|
-
sharedRulesPointer,
|
|
23
20
|
type SharedFileRule,
|
|
24
21
|
} from './shared-file-rules.js';
|
|
25
22
|
export { readAgentPreamble, type AgentPreambleReader, type PreambleWorkspace } from './read-preamble.js';
|