@bevel-software/platform-core-backend 0.25.2 → 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/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/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
|
@@ -14,7 +14,8 @@ import { createToolHandlerFactory } from '../../tool-helpers/tool-handler.js';
|
|
|
14
14
|
import { ToolError, type ToolContext } from '../../tool-helpers/tool.contract.js';
|
|
15
15
|
import type { ToolAuth } from '../../tool-auth/tool-auth.middleware.js';
|
|
16
16
|
import { registerWorkspaceTools } from '../workspace.tools.js';
|
|
17
|
-
import { sharedFileRules, sharedFileRulesSection
|
|
17
|
+
import { sharedFileRules, sharedFileRulesSection } from '../../agent-instructions/shared-file-rules.js';
|
|
18
|
+
import { GUIDE_FIRST_SENTENCE } from '../../tool-registry/guide-first.js';
|
|
18
19
|
import { RoutineWritePolicyService } from '../routine-write-policy.js';
|
|
19
20
|
import { UuidSessionSink, type ISessionSink } from '../session-sink.js';
|
|
20
21
|
import { WorkflowHooks, type AgentOperationContext } from '../../workflow/workflow-hooks.js';
|
|
@@ -98,6 +99,8 @@ let unzipEntries: string[] = [];
|
|
|
98
99
|
let folderTurns: string[] = [];
|
|
99
100
|
/** The policy instance the tools were mounted with, so a test can restrict a session. */
|
|
100
101
|
let writePolicy: RoutineWritePolicyService;
|
|
102
|
+
/** What the platform's guide reads as, for the tests about the guide's name. */
|
|
103
|
+
let guideText = 'THE PLATFORM GUIDE\n';
|
|
101
104
|
/**
|
|
102
105
|
* The focused branch the resolved `ToolContext` carries — mirrors the branch an
|
|
103
106
|
* internal token bakes for the in-process agent. A test sets it to prove a
|
|
@@ -297,7 +300,7 @@ async function start(
|
|
|
297
300
|
recoveryBotEmail: RECOVERY_BOT,
|
|
298
301
|
hooks,
|
|
299
302
|
notes,
|
|
300
|
-
}, writePolicy, {} as never /* sessionSink — start_session not exercised here
|
|
303
|
+
}, writePolicy, {} as never /* sessionSink — start_session not exercised here */, undefined, undefined, undefined, async () => guideText);
|
|
301
304
|
app.use('/api', router);
|
|
302
305
|
httpServer = await new Promise<HttpServer>((r) => {
|
|
303
306
|
const s = app.listen(0, () => r(s));
|
|
@@ -1096,6 +1099,193 @@ describe('write modes and per-path outcomes', () => {
|
|
|
1096
1099
|
});
|
|
1097
1100
|
});
|
|
1098
1101
|
|
|
1102
|
+
/**
|
|
1103
|
+
* The agent guide at the repository root. It is not a file: `read_file` of
|
|
1104
|
+
* its name answers with the platform's guide, after the knowledge base's own
|
|
1105
|
+
* file of that name when it has one, and `file_stat` says what is there.
|
|
1106
|
+
*/
|
|
1107
|
+
describe("the agent guide at the guide's name", () => {
|
|
1108
|
+
const GUIDE = `${KB_DIR}/AGENTS.md`;
|
|
1109
|
+
const read = (base: string, p = GUIDE, extra: Record<string, unknown> = {}) =>
|
|
1110
|
+
post(`${base}/api/agent/tools/read_file`, { path: p, ...extra }).then((r) => r.json() as Promise<{ path: string; content: string }>);
|
|
1111
|
+
const statOf = (base: string, p = GUIDE) =>
|
|
1112
|
+
post(`${base}/api/agent/tools/file_stat`, { path: p }).then((r) => r.json() as Promise<Record<string, unknown>>);
|
|
1113
|
+
|
|
1114
|
+
it('answers with the guide when the knowledge base has no file of that name', async () => {
|
|
1115
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1116
|
+
const base = await start();
|
|
1117
|
+
expect(await read(base)).toEqual({ path: GUIDE, content: 'THE PLATFORM GUIDE\n' });
|
|
1118
|
+
// By the root-anchored and the prefix-less spellings too, like any path.
|
|
1119
|
+
expect((await read(base, `/${GUIDE}`)).content).toBe('THE PLATFORM GUIDE\n');
|
|
1120
|
+
expect((await read(base, 'AGENTS.md')).content).toBe('THE PLATFORM GUIDE\n');
|
|
1121
|
+
// Sliced like any content.
|
|
1122
|
+
expect((await read(base, GUIDE, { offset: 4, limit: 8 })).content).toBe('PLATFORM');
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
it("puts the knowledge base's own AGENTS.md first, then the separator, then the guide", async () => {
|
|
1126
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1127
|
+
const base = await start();
|
|
1128
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nWrite tickets in the present tense.\n');
|
|
1129
|
+
const { content } = await read(base);
|
|
1130
|
+
expect(content.startsWith('# Acme\n\nWrite tickets in the present tense.\n\n---\n')).toBe(true);
|
|
1131
|
+
expect(content.endsWith('\n\nTHE PLATFORM GUIDE\n')).toBe(true);
|
|
1132
|
+
expect(content).toContain("The text above is this knowledge base's own conventions file.");
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
it('never serves a copy of the guide an earlier release left on disk a second time', async () => {
|
|
1136
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1137
|
+
const base = await start();
|
|
1138
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Knowledge base\n\n> **This file is managed by the platform.** Stale.\n');
|
|
1139
|
+
expect((await read(base)).content).toBe('THE PLATFORM GUIDE\n');
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it("never tells a caller who may not read the knowledge base's own file that it exists", async () => {
|
|
1143
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1144
|
+
const denied = await start('read', denyReads(new Set(['AGENTS.md'])));
|
|
1145
|
+
// Nothing of theirs there: the guide is everyone's.
|
|
1146
|
+
const absent = await read(denied);
|
|
1147
|
+
const absentStat = await statOf(denied);
|
|
1148
|
+
expect(absent.content).toBe('THE PLATFORM GUIDE\n');
|
|
1149
|
+
// A file they may not read answers EXACTLY as no file does — a refusal
|
|
1150
|
+
// would be the one thing the platform never says about a restricted
|
|
1151
|
+
// file, which is that it is there.
|
|
1152
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nThe secret conventions.\n');
|
|
1153
|
+
const closed = await post(`${denied}/api/agent/tools/read_file`, { path: GUIDE });
|
|
1154
|
+
expect(closed.status).toBe(200);
|
|
1155
|
+
expect(await closed.json()).toEqual(absent);
|
|
1156
|
+
expect(await statOf(denied)).toEqual(absentStat);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
it("serves the knowledge base's own file to a caller who may read it", async () => {
|
|
1160
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1161
|
+
const allowed = await start('read');
|
|
1162
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nThe conventions.\n');
|
|
1163
|
+
expect((await read(allowed)).content).toContain('The conventions.');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
it("answers the guide alone when the knowledge base's own file vanishes between the probe and the read", async () => {
|
|
1167
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1168
|
+
const base = await start();
|
|
1169
|
+
// The file is there when it is probed and gone when it is read: a
|
|
1170
|
+
// concurrent delete, which is the absent case and never a failure.
|
|
1171
|
+
const probed = fs.stat.bind(fs);
|
|
1172
|
+
let vanish = false;
|
|
1173
|
+
(fs as unknown as Record<string, unknown>).stat = async (p: string) => {
|
|
1174
|
+
const st = await probed(p);
|
|
1175
|
+
if (vanish && p.endsWith('AGENTS.md')) {
|
|
1176
|
+
vanish = false;
|
|
1177
|
+
await fs.deleteFile(p);
|
|
1178
|
+
}
|
|
1179
|
+
return st;
|
|
1180
|
+
};
|
|
1181
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n');
|
|
1182
|
+
vanish = true;
|
|
1183
|
+
expect(await read(base)).toEqual({ path: GUIDE, content: 'THE PLATFORM GUIDE\n' });
|
|
1184
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n');
|
|
1185
|
+
vanish = true;
|
|
1186
|
+
expect(await statOf(base)).toMatchObject({ platformGuide: true });
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('leaves a folder at the guide\'s name to the ordinary stat, and never reads it as a copy', async () => {
|
|
1190
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1191
|
+
const base = await start();
|
|
1192
|
+
await fs.mkdir(`${KB_DIR}/AGENTS.md`);
|
|
1193
|
+
const folder = await statOf(base);
|
|
1194
|
+
expect(folder).toMatchObject({ type: 'directory' });
|
|
1195
|
+
expect(folder.platformGuide).toBeUndefined();
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
it('is a nested AGENTS.md no concern of: that is a file like any other', async () => {
|
|
1199
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1200
|
+
const base = await start();
|
|
1201
|
+
await fs.writeFile(`${KB_DIR}/Handbook/AGENTS.md`, '# Handbook\n');
|
|
1202
|
+
expect((await read(base, `${KB_DIR}/Handbook/AGENTS.md`)).content).toBe('# Handbook\n');
|
|
1203
|
+
expect((await post(`${base}/api/agent/tools/read_file`, { path: `${KB_DIR}/Handbook/HEXIS.md` })).status).toBe(404);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('grep finds the guide where read_file serves it: from the root and at its own path, own file first, denied file absent', async () => {
|
|
1207
|
+
guideText = '# Guide\n\nName every needle you plant.\n';
|
|
1208
|
+
const grep = (base: string, pattern: string, path?: string) =>
|
|
1209
|
+
post(`${base}/api/agent/tools/grep`, path === undefined ? { pattern } : { pattern, path }).then(
|
|
1210
|
+
(r) => r.json() as Promise<{ matches: { path: string; line: number; text: string }[] }>,
|
|
1211
|
+
);
|
|
1212
|
+
const base = await start();
|
|
1213
|
+
await fs.writeFile(`${KB_DIR}/Handbook/a.md`, 'a needle on disk\n');
|
|
1214
|
+
// A search of the whole knowledge base reaches the guide, under the path
|
|
1215
|
+
// a read of it answers to, with the line number that read gives.
|
|
1216
|
+
const fromRoot = await grep(base, 'needle');
|
|
1217
|
+
expect(fromRoot.matches).toContainEqual({ path: GUIDE, line: 3, text: 'Name every needle you plant.' });
|
|
1218
|
+
expect(fromRoot.matches).toContainEqual({ path: `${KB_DIR}/Handbook/a.md`, line: 1, text: 'a needle on disk' });
|
|
1219
|
+
// A search of the guide's own path is a search of what read_file answers.
|
|
1220
|
+
expect((await grep(base, 'needle', GUIDE)).matches).toEqual([{ path: GUIDE, line: 3, text: 'Name every needle you plant.' }]);
|
|
1221
|
+
expect((await grep(base, 'needle', 'AGENTS.md')).matches).toEqual([{ path: GUIDE, line: 3, text: 'Name every needle you plant.' }]);
|
|
1222
|
+
// A search under a folder does not reach a file that is not under it.
|
|
1223
|
+
expect((await grep(base, 'needle', `${KB_DIR}/Handbook`)).matches.map((m) => m.path)).toEqual([`${KB_DIR}/Handbook/a.md`]);
|
|
1224
|
+
|
|
1225
|
+
// With the knowledge base's own AGENTS.md, the search covers the composed
|
|
1226
|
+
// text — the own file first, then the guide — and once: the walk's own
|
|
1227
|
+
// matches in that file are the same lines, so they are not repeated.
|
|
1228
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nOur needle rule.\n');
|
|
1229
|
+
const { content } = await read(base);
|
|
1230
|
+
const guideLine = content.split('\n').indexOf('Name every needle you plant.') + 1;
|
|
1231
|
+
const composed = (await grep(base, 'needle')).matches.filter((m) => m.path === GUIDE);
|
|
1232
|
+
expect(composed).toEqual([
|
|
1233
|
+
{ path: GUIDE, line: 3, text: 'Our needle rule.' },
|
|
1234
|
+
{ path: GUIDE, line: guideLine, text: 'Name every needle you plant.' },
|
|
1235
|
+
]);
|
|
1236
|
+
expect((await grep(base, 'needle', GUIDE)).matches).toEqual(composed);
|
|
1237
|
+
|
|
1238
|
+
// The own file does not use up `max_results` twice: the walk leaves it to
|
|
1239
|
+
// the composed search, so a file later in the tree is still reached when
|
|
1240
|
+
// the own file alone has more matches than the cap. Without that, the
|
|
1241
|
+
// walk filled the cap from AGENTS.md, those matches were dropped as
|
|
1242
|
+
// duplicates, and Handbook/ was never searched.
|
|
1243
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, `# Acme\n\n${'needle\n'.repeat(5)}`);
|
|
1244
|
+
const capped = (await (await post(`${base}/api/agent/tools/grep`, { pattern: 'needle', max_results: 3 })).json()) as {
|
|
1245
|
+
matches: { path: string }[];
|
|
1246
|
+
};
|
|
1247
|
+
expect(capped.matches.map((m) => m.path)).toContain(`${KB_DIR}/Handbook/a.md`);
|
|
1248
|
+
expect(capped.matches).toHaveLength(3);
|
|
1249
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nOur needle rule.\n');
|
|
1250
|
+
|
|
1251
|
+
// A caller who may not read the own file searches the guide alone, from
|
|
1252
|
+
// the root and at the path — as read_file answers them, with no sign that
|
|
1253
|
+
// anything of the organisation's is there.
|
|
1254
|
+
const denied = await start('read', denyReads(new Set(['AGENTS.md'])));
|
|
1255
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n\nOur needle rule.\n');
|
|
1256
|
+
for (const found of [await grep(denied, 'needle'), await grep(denied, 'needle', GUIDE)]) {
|
|
1257
|
+
expect(found.matches.filter((m) => m.path === GUIDE)).toEqual([{ path: GUIDE, line: 3, text: 'Name every needle you plant.' }]);
|
|
1258
|
+
expect(JSON.stringify(found)).not.toContain('Acme');
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it('file_stat says a text file is there to read, and that nothing can be written, moved or deleted at it', async () => {
|
|
1263
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
1264
|
+
const base = await start();
|
|
1265
|
+
expect(await statOf(base)).toMatchObject({
|
|
1266
|
+
name: 'AGENTS.md',
|
|
1267
|
+
type: 'file',
|
|
1268
|
+
size: Buffer.byteLength('THE PLATFORM GUIDE\n'),
|
|
1269
|
+
platformGuide: true,
|
|
1270
|
+
managed: true,
|
|
1271
|
+
movable: false,
|
|
1272
|
+
deletable: false,
|
|
1273
|
+
contentMode: 'text',
|
|
1274
|
+
textEditable: false,
|
|
1275
|
+
access: { read: true, write: false },
|
|
1276
|
+
});
|
|
1277
|
+
// With a file of the knowledge base's own there, stat describes THAT file.
|
|
1278
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n');
|
|
1279
|
+
const own = await statOf(base);
|
|
1280
|
+
expect(own).toMatchObject({ name: 'AGENTS.md', type: 'file', managed: false, movable: true, textEditable: true });
|
|
1281
|
+
expect(own.platformGuide).toBeUndefined();
|
|
1282
|
+
// A copy an earlier release wrote is what read_file does not serve, so
|
|
1283
|
+
// stat says the same thing it says for no file at all.
|
|
1284
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Knowledge base\n\n> **This file is managed by the platform.** Stale.\n');
|
|
1285
|
+
expect(await statOf(base)).toMatchObject({ platformGuide: true, managed: true, movable: false });
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1099
1289
|
describe('read-permission gating', () => {
|
|
1100
1290
|
// Seed two KB nodes; the access stub denies read on the "secret" one.
|
|
1101
1291
|
async function startGated(): Promise<string> {
|
|
@@ -1714,19 +1904,18 @@ describe('office documents and PDFs', () => {
|
|
|
1714
1904
|
expect(await readContent(base, `${KB_DIR}/deck.pptx`)).toContain('Original');
|
|
1715
1905
|
});
|
|
1716
1906
|
|
|
1717
|
-
it('every mounted tool
|
|
1907
|
+
it('every mounted tool opens with the one sentence sending the agent to the guide, and repeats none of the rules', async () => {
|
|
1718
1908
|
await start();
|
|
1719
1909
|
const tools = await toolRegistry.listInternal();
|
|
1720
|
-
const pointer = sharedRulesPointer(testKbContext().layout);
|
|
1721
1910
|
// The shell is in the list too: it carried the agent-guide reminder before,
|
|
1722
1911
|
// and that reminder is one of the rules that moved.
|
|
1723
1912
|
const mounted = ['read_file', 'list_files', 'file_stat', 'grep', 'write_file', 'write_files', 'edit_file', 'delete_file', 'delete_folder', 'mkdir', 'move_file', 'copy_file', 'unzip', 'execute_command'];
|
|
1724
1913
|
for (const name of mounted) {
|
|
1725
1914
|
const def = tools.find((t) => t.name === name);
|
|
1726
1915
|
expect(def, name).toBeDefined();
|
|
1727
|
-
expect(def!.description!.
|
|
1728
|
-
// Once, at the
|
|
1729
|
-
expect(def!.description!.split(
|
|
1916
|
+
expect(def!.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `), name).toBe(true);
|
|
1917
|
+
// Once, at the front — not once per paragraph that used to be appended.
|
|
1918
|
+
expect(def!.description!.split(GUIDE_FIRST_SENTENCE), name).toHaveLength(2);
|
|
1730
1919
|
// EVERY shared rule, in full, is in the two shared places now (see
|
|
1731
1920
|
// agent-instructions/__tests__/shared-file-rules.test.ts) and in no
|
|
1732
1921
|
// description. Checked on the whole body rather than on a phrase: the
|
|
@@ -1740,10 +1929,11 @@ describe('office documents and PDFs', () => {
|
|
|
1740
1929
|
expect(def!.description, name).not.toContain('Content rule (the same on every file tool)');
|
|
1741
1930
|
expect(def!.description, name).not.toContain('Before your first read or change in a workspace');
|
|
1742
1931
|
}
|
|
1743
|
-
// start_session
|
|
1744
|
-
//
|
|
1932
|
+
// start_session touches no file, yet it opens with the same sentence: the
|
|
1933
|
+
// guide is read before ANYTHING in the platform, a session included.
|
|
1934
|
+
// External-only, so it is looked up on that surface.
|
|
1745
1935
|
const external = await toolRegistry.listExternal();
|
|
1746
|
-
expect(external.find((t) => t.name === 'start_session')!.description).
|
|
1936
|
+
expect(external.find((t) => t.name === 'start_session')!.description!.startsWith(`${GUIDE_FIRST_SENTENCE} `)).toBe(true);
|
|
1747
1937
|
});
|
|
1748
1938
|
|
|
1749
1939
|
describe('binary capability contract: a text file, a document, an image and a zip', () => {
|
|
@@ -2998,19 +3188,17 @@ describe('preflight for moves and deletes', () => {
|
|
|
2998
3188
|
expect(await exists(args.src)).toBe(true);
|
|
2999
3189
|
});
|
|
3000
3190
|
|
|
3001
|
-
it('every one of the
|
|
3191
|
+
it('every one of the three platform files gets the same sentence, and the agent never gets the admin restore', async () => {
|
|
3002
3192
|
// The agent move tool has no exception: the recovery move is a person's,
|
|
3003
3193
|
// made as an admin, and an agent is neither.
|
|
3004
3194
|
const base = await seeded();
|
|
3005
3195
|
await fs.writeFile(KB('roles.yaml'), 'roles: {}\n');
|
|
3006
|
-
await fs.writeFile(KB('AGENTS.md'), 'agents\n');
|
|
3007
3196
|
await fs.writeFile(KB('.bevelignore'), '*.tmp\n');
|
|
3008
3197
|
await fs.writeFile(KB('Misplaced/access.md'), '---\nread: everyone\n---\n');
|
|
3009
3198
|
const cases: [string, string][] = [
|
|
3010
3199
|
[KB('access.md'), KB('Sales/access.md')],
|
|
3011
3200
|
[KB('roles.yaml'), KB('Sales/roles.yaml')],
|
|
3012
3201
|
[KB('.bevelignore'), KB('Sales/.bevelignore')],
|
|
3013
|
-
[KB('AGENTS.md'), KB('Sales/AGENTS.md')],
|
|
3014
3202
|
// Including the shape of the admin's recovery move: a misplaced
|
|
3015
3203
|
// access.md into a folder that has none. A person holding the Admin
|
|
3016
3204
|
// role is allowed exactly this move from the UI; the agent is not.
|
|
@@ -3120,15 +3308,19 @@ describe('preflight for moves and deletes', () => {
|
|
|
3120
3308
|
describe('what counts as the platform\'s own, and what a move or delete may reach', () => {
|
|
3121
3309
|
const caseSensitiveDisk = process.platform === 'linux';
|
|
3122
3310
|
|
|
3123
|
-
it('roles.yaml
|
|
3311
|
+
it('roles.yaml is a platform file only at the root; access.md and .bevelignore at any depth; AGENTS.md nowhere', async () => {
|
|
3124
3312
|
const base = await seeded();
|
|
3125
3313
|
await fs.writeFile(KB('roles.yaml'), 'roles: {}\n');
|
|
3126
3314
|
await fs.writeFile(KB('Sales/roles.yaml'), 'content');
|
|
3315
|
+
await fs.writeFile(KB('AGENTS.md'), 'content');
|
|
3127
3316
|
await fs.writeFile(KB('Sales/AGENTS.md'), 'content');
|
|
3128
3317
|
await fs.writeFile(KB('Sales/.bevelignore'), '*.tmp\n');
|
|
3129
3318
|
const managed = async (p: string) => (await call(base, 'file_stat', { path: KB(p) })).body.managed;
|
|
3130
3319
|
expect(await managed('roles.yaml')).toBe(true);
|
|
3131
3320
|
expect(await managed('Sales/roles.yaml')).toBe(false);
|
|
3321
|
+
// The organisation's own conventions file: the guide is served from
|
|
3322
|
+
// code, so nothing under this name is the platform's.
|
|
3323
|
+
expect(await managed('AGENTS.md')).toBe(false);
|
|
3132
3324
|
expect(await managed('Sales/AGENTS.md')).toBe(false);
|
|
3133
3325
|
expect(await managed('Sales/.bevelignore')).toBe(true);
|
|
3134
3326
|
expect((await call(base, 'move_file', { src: KB('Sales/roles.yaml'), dest: KB('Sales/old-roles.yaml') })).body).toMatchObject({ moved: true });
|
|
@@ -3497,7 +3689,7 @@ describe('preflight for moves and deletes', () => {
|
|
|
3497
3689
|
// is stated once in the shared rules rather than on each of them.
|
|
3498
3690
|
expect(sharedFileRulesSection(testKbContext().layout)).toContain('`write-denied`');
|
|
3499
3691
|
for (const name of ['move_file', 'delete_file', 'delete_folder']) {
|
|
3500
|
-
expect((await def(name)).description, name).
|
|
3692
|
+
expect((await def(name)).description.startsWith(GUIDE_FIRST_SENTENCE), name).toBe(true);
|
|
3501
3693
|
}
|
|
3502
3694
|
});
|
|
3503
3695
|
});
|
|
@@ -4015,6 +4207,47 @@ describe('agent read/write hooks', () => {
|
|
|
4015
4207
|
expect(writes).toEqual([]);
|
|
4016
4208
|
});
|
|
4017
4209
|
|
|
4210
|
+
it("file_stat at the guide's name tells the read hook once when it reads the organisation's own file, and never for the guide alone", async () => {
|
|
4211
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
4212
|
+
const base = await start();
|
|
4213
|
+
record();
|
|
4214
|
+
// Nothing of the organisation's there: the guide is everyone's and no
|
|
4215
|
+
// file is read, so the hook hears nothing.
|
|
4216
|
+
expect((await post(`${base}/api/agent/tools/file_stat`, { path: `${KB_DIR}/AGENTS.md`, sessionId: 's1' })).status).toBe(200);
|
|
4217
|
+
expect(reads).toEqual([]);
|
|
4218
|
+
// The organisation's own file: telling it from a stale copy reads it,
|
|
4219
|
+
// and the hook hears of that read exactly once — as it does of a read_file.
|
|
4220
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme\n');
|
|
4221
|
+
expect((await post(`${base}/api/agent/tools/file_stat`, { path: `${KB_DIR}/AGENTS.md`, sessionId: 's1' })).status).toBe(200);
|
|
4222
|
+
expect(reads.map((op) => op.wsPath)).toEqual([`${KB_DIR}/AGENTS.md`]);
|
|
4223
|
+
// A stale copy of the guide is read to be recognised, so it is heard of too.
|
|
4224
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Knowledge base\n\n> **This file is managed by the platform.** Stale.\n');
|
|
4225
|
+
expect((await post(`${base}/api/agent/tools/file_stat`, { path: `${KB_DIR}/AGENTS.md`, sessionId: 's1' })).status).toBe(200);
|
|
4226
|
+
expect(reads.map((op) => op.wsPath)).toEqual([`${KB_DIR}/AGENTS.md`, `${KB_DIR}/AGENTS.md`]);
|
|
4227
|
+
});
|
|
4228
|
+
|
|
4229
|
+
it("grep at the guide's name tells the read hook the same way: never for the guide alone, once for the organisation's own file", async () => {
|
|
4230
|
+
guideText = 'THE PLATFORM GUIDE\n';
|
|
4231
|
+
const base = await start();
|
|
4232
|
+
record();
|
|
4233
|
+
// The guide alone: nothing of the organisation's is read, so the hook
|
|
4234
|
+
// hears nothing — a search of the guide's path is not a read of a file.
|
|
4235
|
+
expect((await post(`${base}/api/agent/tools/grep`, { pattern: 'GUIDE', path: `${KB_DIR}/AGENTS.md`, sessionId: 's1' })).status).toBe(200);
|
|
4236
|
+
expect(reads).toEqual([]);
|
|
4237
|
+
// The organisation's own file: once, as read_file tells it — not once
|
|
4238
|
+
// for the search root and again for the file.
|
|
4239
|
+
await fs.writeFile(`${KB_DIR}/AGENTS.md`, '# Acme GUIDE\n');
|
|
4240
|
+
expect((await post(`${base}/api/agent/tools/grep`, { pattern: 'GUIDE', path: `${KB_DIR}/AGENTS.md`, sessionId: 's1' })).status).toBe(200);
|
|
4241
|
+
expect(reads.map((op) => op.wsPath)).toEqual([`${KB_DIR}/AGENTS.md`]);
|
|
4242
|
+
// A search of the whole knowledge base: the root once, then each file the
|
|
4243
|
+
// walk opens once — the own AGENTS.md among them exactly once, from the
|
|
4244
|
+
// composed search, never again from the walk.
|
|
4245
|
+
reads = [];
|
|
4246
|
+
expect((await post(`${base}/api/agent/tools/grep`, { pattern: 'GUIDE', sessionId: 's1' })).status).toBe(200);
|
|
4247
|
+
expect(reads[0]?.wsPath).toBe(KB_DIR);
|
|
4248
|
+
expect(reads.filter((op) => op.wsPath === `${KB_DIR}/AGENTS.md`)).toHaveLength(1);
|
|
4249
|
+
});
|
|
4250
|
+
|
|
4018
4251
|
it('the read hook covers list_files, file_stat, grep, delete_file and delete_folder', async () => {
|
|
4019
4252
|
const base = await start();
|
|
4020
4253
|
record();
|
|
@@ -4275,22 +4508,22 @@ describe('tool descriptions and the deployment note', () => {
|
|
|
4275
4508
|
expect(all.get('read_file')!.description).not.toContain('Stay within one');
|
|
4276
4509
|
});
|
|
4277
4510
|
|
|
4278
|
-
it("a registered note lands after every gated tool's own text,
|
|
4511
|
+
it("a registered note lands after every gated tool's own text, behind the guide-first opening, and on the sessionId input", async () => {
|
|
4279
4512
|
await start();
|
|
4280
4513
|
notes.registerGatedToolNote(' One folder per conversation.');
|
|
4281
4514
|
notes.registerSessionIdNote(' It also pins that folder.');
|
|
4282
4515
|
const all = await defs();
|
|
4283
|
-
// The
|
|
4284
|
-
//
|
|
4285
|
-
// deployment's note
|
|
4286
|
-
const pointer = sharedRulesPointer(testKbContext().layout);
|
|
4516
|
+
// The guide-first sentence is FIRST, always: that is the one instruction
|
|
4517
|
+
// an agent needs, and a description cut short from the end must not lose
|
|
4518
|
+
// it. The deployment's note is last, after the tool's own text.
|
|
4287
4519
|
for (const name of ['read_file', 'list_files', 'file_stat', 'grep', 'write_file', 'write_files', 'edit_file', 'delete_file', 'delete_folder', 'mkdir', 'move_file', 'copy_file', 'unzip']) {
|
|
4288
|
-
expect(all.get(name)!.description.
|
|
4520
|
+
expect(all.get(name)!.description.startsWith(`${GUIDE_FIRST_SENTENCE} `), name).toBe(true);
|
|
4521
|
+
expect(all.get(name)!.description.endsWith(' One folder per conversation.'), name).toBe(true);
|
|
4289
4522
|
expect(sessionIdDescriptionOf(all.get(name)!), name).toBe(`${SESSION_ID_DESCRIPTION} It also pins that folder.`);
|
|
4290
4523
|
}
|
|
4291
4524
|
// `execute_command` is internal-only, so it is checked on that surface.
|
|
4292
4525
|
const internal = new Map((await toolRegistry.listInternal()).map((t) => [t.name, t]));
|
|
4293
|
-
expect(internal.get('execute_command')!.description.endsWith(
|
|
4526
|
+
expect(internal.get('execute_command')!.description.endsWith(' One folder per conversation.')).toBe(true);
|
|
4294
4527
|
});
|
|
4295
4528
|
|
|
4296
4529
|
it('a tool that is not gated carries no note', async () => {
|