@bevel-software/platform-shared 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/dist/git/pr.types.d.ts +101 -0
- package/dist/git/pr.types.d.ts.map +1 -1
- package/dist/git/types.d.ts +70 -1
- package/dist/git/types.d.ts.map +1 -1
- package/dist/workflow/interface.d.ts +12 -0
- package/dist/workflow/interface.d.ts.map +1 -1
- package/dist/workspace/kb-layout.d.ts +40 -101
- package/dist/workspace/kb-layout.d.ts.map +1 -1
- package/dist/workspace/kb-layout.js +40 -159
- package/dist/workspace/kb-layout.js.map +1 -1
- package/dist/workspace/platform-files.d.ts +21 -24
- package/dist/workspace/platform-files.d.ts.map +1 -1
- package/dist/workspace/platform-files.js +23 -25
- package/dist/workspace/platform-files.js.map +1 -1
- package/package.json +1 -1
- package/src/git/pr.types.ts +99 -0
- package/src/git/types.ts +76 -1
- package/src/workflow/interface.ts +11 -0
- package/src/workspace/kb-layout.ts +39 -172
- package/src/workspace/platform-files.ts +23 -26
package/src/git/pr.types.ts
CHANGED
|
@@ -41,8 +41,33 @@ export interface PullRequestSummary {
|
|
|
41
41
|
base: string;
|
|
42
42
|
state: PullRequestState;
|
|
43
43
|
createdAt: string;
|
|
44
|
+
/**
|
|
45
|
+
* The latest moment the change-request ROW records: the latest of its
|
|
46
|
+
* creation time, its `updated_at` stamp (written when the row itself
|
|
47
|
+
* changes — a merge, a close, a recorded apply failure) and its close time.
|
|
48
|
+
* GitHub's `updated_at` on a pull request means "when anything about it
|
|
49
|
+
* last changed"; comments and approvals carry their own times here and do
|
|
50
|
+
* not move this one, so this is as close as the row can honestly answer —
|
|
51
|
+
* never a time guessed from elsewhere. Absent only on a summary built by
|
|
52
|
+
* something other than a change-request row (test doubles).
|
|
53
|
+
*/
|
|
54
|
+
updatedAt?: string;
|
|
44
55
|
/** Relative paths within `knowledge-base/`. Empty if not yet computed. */
|
|
45
56
|
touchedNodePaths: string[];
|
|
57
|
+
/**
|
|
58
|
+
* The same changed files as {@link touchedNodePaths}, paired with the path
|
|
59
|
+
* each was renamed from — what a READ gate has to decide over, since
|
|
60
|
+
* `touchedNodePaths` reports a rename under its new name alone and so cannot
|
|
61
|
+
* say that a readable-looking file came out of a folder its reader cannot
|
|
62
|
+
* open.
|
|
63
|
+
*
|
|
64
|
+
* Absent when no summary builder filled it (a test double, a caller that
|
|
65
|
+
* constructs a summary by hand). A read gate must treat absence as "nothing
|
|
66
|
+
* proven" rather than falling back to `touchedNodePaths` — that fallback is
|
|
67
|
+
* precisely the disagreement between a list and a detail this field exists to
|
|
68
|
+
* remove, and it would come back silently.
|
|
69
|
+
*/
|
|
70
|
+
touchedNodeFiles?: ChangedPathPair[];
|
|
46
71
|
review: PullRequestReviewStatus;
|
|
47
72
|
/**
|
|
48
73
|
* Link to the change request: absolute (`<public frontend address>/change-requests/<number>`)
|
|
@@ -79,6 +104,69 @@ export interface ChangeRequestApplyFailure {
|
|
|
79
104
|
byName: string;
|
|
80
105
|
}
|
|
81
106
|
|
|
107
|
+
/**
|
|
108
|
+
* One changed file of a change request, named on both sides: where it is now
|
|
109
|
+
* and, for a rename, where it came from.
|
|
110
|
+
*
|
|
111
|
+
* The minimum a READ gate needs. A plain path list says a rename's new name and
|
|
112
|
+
* (with `forAccessCheck`) its old one, but not that the two are the same file —
|
|
113
|
+
* and the diff of a rename shows the old side's content, so a file moved out of
|
|
114
|
+
* a folder its reader cannot open must not be offered under its new name
|
|
115
|
+
* either. Both the list of change requests and the detail of one decide
|
|
116
|
+
* readability over these, so the two cannot disagree about whether a request is
|
|
117
|
+
* visible.
|
|
118
|
+
*/
|
|
119
|
+
export interface ChangedPathPair {
|
|
120
|
+
path: string;
|
|
121
|
+
previousPath?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* An APPLIED change request, named by BOTH the commit its row records and the
|
|
126
|
+
* number that row carries — because reading the commit's own change is only
|
|
127
|
+
* sound if the commit really is that request's merge commit, and the number is
|
|
128
|
+
* what proves it (the merge commit's subject ends with `(#<number>)`).
|
|
129
|
+
*
|
|
130
|
+
* The pair travels together so no read can ask for a commit without saying which
|
|
131
|
+
* request it must belong to. `merged_sha` is not reliably a commit the request
|
|
132
|
+
* created: a merge with nothing to merge used to record the TARGET TIP, which in
|
|
133
|
+
* a deployment that lands everything through change requests is usually ANOTHER
|
|
134
|
+
* request's merge commit.
|
|
135
|
+
*/
|
|
136
|
+
export interface AppliedChangeRef {
|
|
137
|
+
/** The change request's number, as its merge commit's subject names it. */
|
|
138
|
+
number: number;
|
|
139
|
+
/** The `merged_sha` the row records. */
|
|
140
|
+
mergeSha: string;
|
|
141
|
+
/**
|
|
142
|
+
* The request's stored title, which lets a merge commit written by an
|
|
143
|
+
* earlier release — the title not yet flattened, so a blank line in it put
|
|
144
|
+
* the number after git's subject — be recognised as the request's own.
|
|
145
|
+
*/
|
|
146
|
+
title?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* What applying a change request did.
|
|
151
|
+
*
|
|
152
|
+
* Two different commits, which is why they are two fields:
|
|
153
|
+
*
|
|
154
|
+
* `sha` is the state the target is left at — the tip at which the request
|
|
155
|
+
* counts as merged. When there was nothing to merge it is whatever landed on
|
|
156
|
+
* the target last, usually ANOTHER request's merge commit, so it must never be
|
|
157
|
+
* recorded as this request's own: a reader would answer with that other
|
|
158
|
+
* request's files under this number.
|
|
159
|
+
*
|
|
160
|
+
* `mergeCommit` is the commit this request OWNS, and the only sha a row may
|
|
161
|
+
* record as its `merged_sha` — the commit this merge wrote, or the one an
|
|
162
|
+
* earlier attempt wrote and left on the target when it failed to finalize the
|
|
163
|
+
* row. Null when the request has none, which is the genuinely empty case: its
|
|
164
|
+
* file list is empty anyway, so nothing readable is lost.
|
|
165
|
+
*/
|
|
166
|
+
export type AppliedMergeResult =
|
|
167
|
+
| { kind: 'merged'; sha: string; mergeCommit: string | null }
|
|
168
|
+
| { kind: 'conflicts'; paths: string[] };
|
|
169
|
+
|
|
82
170
|
export type PrFileStatus =
|
|
83
171
|
| 'added'
|
|
84
172
|
| 'modified'
|
|
@@ -368,6 +456,17 @@ export interface IPullRequestService {
|
|
|
368
456
|
* via `gh pr create`) and the user expects to see the update immediately.
|
|
369
457
|
*/
|
|
370
458
|
listOpenPrs(opts?: { fresh?: boolean }): Promise<PullRequestSummary[]>;
|
|
459
|
+
/**
|
|
460
|
+
* PRs in ANY of `states`, newest first — what `listOpenPrs` answers for the
|
|
461
|
+
* open ones, widened to the closed and merged rows it filters out. Only the
|
|
462
|
+
* `['open']` case goes through that method's 30s list cache; a read that
|
|
463
|
+
* asks for closed rows is rare (a reader catching up on what happened) and
|
|
464
|
+
* is served straight from the table.
|
|
465
|
+
*/
|
|
466
|
+
listPrsByState(
|
|
467
|
+
states: PullRequestState[],
|
|
468
|
+
opts?: { fresh?: boolean; workspaceId?: string },
|
|
469
|
+
): Promise<PullRequestSummary[]>;
|
|
371
470
|
listPrsAuthoredBy(githubLoginOrEmail: string): Promise<PullRequestSummary[]>;
|
|
372
471
|
/**
|
|
373
472
|
* PRs whose touched paths have an owner with the given email.
|
package/src/git/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AuthUser } from '../auth/types.js';
|
|
2
|
-
import type { PullRequestFile } from './pr.types.js';
|
|
2
|
+
import type { AppliedChangeRef, ChangedPathPair, PullRequestFile } from './pr.types.js';
|
|
3
3
|
|
|
4
4
|
export interface BranchInfo {
|
|
5
5
|
name: string;
|
|
@@ -278,6 +278,81 @@ export interface IGitService {
|
|
|
278
278
|
opts?: { fetch?: boolean },
|
|
279
279
|
): Promise<string[]>;
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* `changedPathsForPr`'s answer AND the same diff left as rename-aware pairs,
|
|
283
|
+
* from ONE `git diff`.
|
|
284
|
+
*
|
|
285
|
+
* The flat list cannot pair a rename's two paths: git reports a rename under
|
|
286
|
+
* its new name, and `forAccessCheck` adds the old name to the same
|
|
287
|
+
* undifferentiated set. That union is enough to authorize a WRITE ("may they
|
|
288
|
+
* touch everything this lands?"), but not to decide a READ — a file renamed
|
|
289
|
+
* out of a folder the caller cannot open is readable under neither of its
|
|
290
|
+
* names, since the diff of a rename shows the old side's content, and
|
|
291
|
+
* deciding that needs to know which old path belongs to which new file. The
|
|
292
|
+
* change-request read tools gate their file lists on exactly this, and the
|
|
293
|
+
* list of requests must reach the same verdict as the detail of one.
|
|
294
|
+
*/
|
|
295
|
+
changedPathsAndPairsForPr(
|
|
296
|
+
workspaceId: string,
|
|
297
|
+
baseBranch: string,
|
|
298
|
+
headBranch: string,
|
|
299
|
+
opts?: { fetch?: boolean },
|
|
300
|
+
): Promise<{ paths: string[]; pairs: ChangedPathPair[] }>;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The changed-file list an APPLIED change request landed, read from its merge
|
|
304
|
+
* commit against that commit's first parent.
|
|
305
|
+
*
|
|
306
|
+
* Its source branch is retired, so the branch pair the other two methods want
|
|
307
|
+
* no longer exists; what is left is the merge commit the row records. Its
|
|
308
|
+
* first parent is the target as it stood before the merge, so the two-dot diff
|
|
309
|
+
* between them is precisely what the request applied — and immutable, which
|
|
310
|
+
* the branch pair never was.
|
|
311
|
+
*
|
|
312
|
+
* The commit is VERIFIED to be that request's own, which is why the ref
|
|
313
|
+
* carries the number as well as the sha: a merge with nothing to merge used to
|
|
314
|
+
* record the target tip, and reading that commit's change would answer with
|
|
315
|
+
* another request's files. It must be in the clone, have a second parent, and
|
|
316
|
+
* carry a subject naming this request; anything else rejects with
|
|
317
|
+
* `WorkflowValidationError`, which every caller reads as "the file set could
|
|
318
|
+
* not be resolved" and answers fail-closed (no files, so author-only) rather
|
|
319
|
+
* than reaching for a fetch per request.
|
|
320
|
+
*
|
|
321
|
+
* No network, either way.
|
|
322
|
+
*/
|
|
323
|
+
changedFilesOfAppliedChange(
|
|
324
|
+
workspaceId: string,
|
|
325
|
+
applied: AppliedChangeRef,
|
|
326
|
+
opts?: { patchCap?: number },
|
|
327
|
+
): Promise<PullRequestFile[]>;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The same applied change as the two path views a change-request SUMMARY
|
|
331
|
+
* needs, out of one `git diff` — `changedPathsAndPairsForPr` for an applied
|
|
332
|
+
* request instead of a branch pair, with the same verification and the same
|
|
333
|
+
* no-network contract as {@link changedFilesOfAppliedChange}.
|
|
334
|
+
*/
|
|
335
|
+
changedPathsAndPairsOfAppliedChange(
|
|
336
|
+
workspaceId: string,
|
|
337
|
+
applied: AppliedChangeRef,
|
|
338
|
+
): Promise<{ paths: string[]; pairs: ChangedPathPair[] }>;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The two commits an APPLIED change request spanned, recovered from its merge
|
|
342
|
+
* commit: the target before the merge (`^1`) and the source tip that was
|
|
343
|
+
* merged (`^2`).
|
|
344
|
+
*
|
|
345
|
+
* The head matters beyond being informative: an approval is called stale when
|
|
346
|
+
* the head it was given against is not the detail's `headSha`, so answering
|
|
347
|
+
* the merge commit there would report every approval a merged request ever
|
|
348
|
+
* collected as stale. Same verification, same no-network contract and the same
|
|
349
|
+
* `WorkflowValidationError` as {@link changedFilesOfAppliedChange}.
|
|
350
|
+
*/
|
|
351
|
+
appliedChangeShas(
|
|
352
|
+
workspaceId: string,
|
|
353
|
+
applied: AppliedChangeRef,
|
|
354
|
+
): Promise<{ baseSha: string; headSha: string }>;
|
|
355
|
+
|
|
281
356
|
/**
|
|
282
357
|
* A change request's fork point (merge base of the two resolved commits)
|
|
283
358
|
* and whether the target has commits the proposal does not contain. No
|
|
@@ -437,6 +437,17 @@ export interface IWorkflowService {
|
|
|
437
437
|
// ── Change Requests ───────────────────────────────────────────────────────
|
|
438
438
|
|
|
439
439
|
listChangeRequests(opts?: { fresh?: boolean }): Promise<ChangeRequest[]>;
|
|
440
|
+
/**
|
|
441
|
+
* Change requests in ANY of `states`, newest first. `listChangeRequests`
|
|
442
|
+
* answers the open ones only — the app's lists are all about what is still
|
|
443
|
+
* being decided — so a reader catching up on what HAPPENED (the agent read
|
|
444
|
+
* tools' `state: closed` / `state: all`) needs this one.
|
|
445
|
+
*/
|
|
446
|
+
listChangeRequestsByState(
|
|
447
|
+
states: ChangeRequestState[],
|
|
448
|
+
/** `workspaceId`: the clone to read file lists in; any clone will do, and a caller that resolved one passes it so the list and the by-number reads agree. */
|
|
449
|
+
opts?: { fresh?: boolean; workspaceId?: string },
|
|
450
|
+
): Promise<ChangeRequest[]>;
|
|
440
451
|
/** Change requests authored by the given user (matched on stored author identity). */
|
|
441
452
|
listChangeRequestsAuthoredBy(
|
|
442
453
|
emailOrLogin: string,
|
|
@@ -111,31 +111,30 @@ export let SKILLS_DIR = 'Skills';
|
|
|
111
111
|
export let PLUGINS_DIR = 'Plugins';
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* The
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* A live binding like the three roots above — read it inside a function body,
|
|
128
|
-
* never capture it at module scope.
|
|
114
|
+
* The name the platform's agent guide is READ BY at the repository root — the
|
|
115
|
+
* document every connected agent is told to read first.
|
|
116
|
+
*
|
|
117
|
+
* The guide is no longer a file: the backend composes it from text the code
|
|
118
|
+
* owns and serves it through `get_agent_guide` and through a `read_file` of
|
|
119
|
+
* this name (see core-backend's `modules/agent-guide`), after the knowledge
|
|
120
|
+
* base's own file of that name when it has one. ONE name on every
|
|
121
|
+
* deployment: the setting that let a deployment rename the written guide is
|
|
122
|
+
* gone, and the settings layer no longer reads a value it saved — the layout
|
|
123
|
+
* it produces carries this name. The field stays on the layout for the shape
|
|
124
|
+
* every consumer reads, and the helpers here (`agentsFileOf`,
|
|
125
|
+
* `configureKbLayout`) still apply whatever a caller puts in it, so the one
|
|
126
|
+
* name is the producer's doing, not this module's.
|
|
129
127
|
*/
|
|
130
128
|
export let AGENTS_FILE = 'AGENTS.md';
|
|
131
129
|
|
|
132
130
|
/**
|
|
133
|
-
* The name the
|
|
131
|
+
* The name the guide had when it was the only name it could have, and the
|
|
132
|
+
* name it is always read by now.
|
|
134
133
|
*
|
|
135
134
|
* Referenced ONLY by the code that has to tell OUR file from THEIRS — the
|
|
136
|
-
* boot-time removal of
|
|
137
|
-
* stops hiding
|
|
138
|
-
*
|
|
135
|
+
* boot-time removal of the guide copies earlier releases wrote, the ignore
|
|
136
|
+
* rule that stops hiding them, the instruction telling an agent what to read
|
|
137
|
+
* first. In the spirit of {@link LEGACY_GROUPS_DIR}: a second live spelling
|
|
139
138
|
* of the CURRENT name is how two layouts start being supported by accident, so
|
|
140
139
|
* this one is a constant and means exactly one thing.
|
|
141
140
|
*/
|
|
@@ -143,11 +142,11 @@ export const LEGACY_AGENTS_FILE = 'AGENTS.md';
|
|
|
143
142
|
|
|
144
143
|
/**
|
|
145
144
|
* The platform files whose names are FIXED — the ones no deployment renames.
|
|
146
|
-
* The guide is
|
|
147
|
-
*
|
|
148
|
-
*
|
|
145
|
+
* The guide is NOT a platform file: nothing is written under its name, so a
|
|
146
|
+
* root `AGENTS.md` (or whatever the deployment once called the guide) is the
|
|
147
|
+
* organisation's own conventions page, movable and deletable like any other.
|
|
149
148
|
*
|
|
150
|
-
* It lives in this module rather than beside
|
|
149
|
+
* It lives in this module rather than beside `platform-files.ts` because the
|
|
151
150
|
* LAYOUT has to validate against it (a guide may not be called `access.md`),
|
|
152
151
|
* and `platform-files.ts` already reads this module — the other direction
|
|
153
152
|
* would be a cycle.
|
|
@@ -196,139 +195,13 @@ export function agentsFileOf(layout: KbLayout): string {
|
|
|
196
195
|
* a leading `#` is a comment and a leading `!` a negation (the rule would
|
|
197
196
|
* silently hide nothing), `[`, `]`, `*` and `?` are globs, and a backslash is
|
|
198
197
|
* the escape itself. Each is escaped so the pattern names exactly the file.
|
|
198
|
+
* Read by the startup step that takes the platform's own rule for the guide
|
|
199
|
+
* OUT of an ignore file an earlier release wrote it into.
|
|
199
200
|
*/
|
|
200
201
|
export function gitignoreLiteral(name: string): string {
|
|
201
202
|
return name.replace(/[[\]*?\\]/g, '\\$&').replace(/^([#!])/, '\\$1');
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
/**
|
|
205
|
-
* The ONE sentence the platform offers to keep in a customer's own
|
|
206
|
-
* `AGENTS.md`, pointing at the managed guide beside it.
|
|
207
|
-
*
|
|
208
|
-
* Defined here, once, because two surfaces must produce the identical text:
|
|
209
|
-
* the deployment-settings field previews it before the admin consents, and the
|
|
210
|
-
* startup step appends it. A sentence written twice is a sentence that drifts,
|
|
211
|
-
* and a drifted one appends a SECOND copy to every customer file on the boot
|
|
212
|
-
* after the drift — which is the one thing this whole feature exists to stop.
|
|
213
|
-
*
|
|
214
|
-
* A guide name is a FILE NAME, not an identifier: everything `validateFilename`
|
|
215
|
-
* admits can appear in it — spaces, brackets, parentheses, `#`, `%` — and each
|
|
216
|
-
* of those means something in an inline link. So the link is BUILT rather than
|
|
217
|
-
* interpolated: the label backslash-escaped ({@link markdownLinkLabel}), the
|
|
218
|
-
* destination percent-encoded ({@link agentsFileLinkPath}). A name that only
|
|
219
|
-
* parenthesised would break the destination; `#` would turn the rest of the
|
|
220
|
-
* name into a URL fragment, and the link would point at the customer's own
|
|
221
|
-
* file.
|
|
222
|
-
*
|
|
223
|
-
* Neither spelling need match the name as it is on disk, so nothing may ask
|
|
224
|
-
* whether this sentence is present by searching for the RAW name — see
|
|
225
|
-
* {@link mentionsAgentsFile}, which is how the startup step asks.
|
|
226
|
-
*/
|
|
227
|
-
export function agentsFilePointerSentence(agentsFile: string): string {
|
|
228
|
-
return `Read [${markdownLinkLabel(agentsFile)}](${agentsFileLinkPath(agentsFile)})${POINTER_SENTENCE_TAIL}`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Everything of the sentence that does NOT depend on the guide's name — split
|
|
233
|
-
* out so the one definition above can also be RECOGNISED, by the pattern below,
|
|
234
|
-
* when the name it was written with is no longer the name in effect.
|
|
235
|
-
*/
|
|
236
|
-
const POINTER_SENTENCE_TAIL =
|
|
237
|
-
" before working in this knowledge base — it is the platform's guide to its layout, files and rules.";
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* A pointer sentence the platform wrote, naming ANY guide.
|
|
241
|
-
*
|
|
242
|
-
* The label admits a backslash escape (`\[`, `\]`) because that is what
|
|
243
|
-
* {@link markdownLinkLabel} puts there; the destination cannot contain a `)`
|
|
244
|
-
* or a newline because {@link agentsFileLinkPath} encodes both. Anchored at
|
|
245
|
-
* both ends by text the platform fixed, so the shape is the provenance —
|
|
246
|
-
* a customer would have to reproduce our sentence word for word to be taken
|
|
247
|
-
* for us, which is the same bar the managed-guide header sets.
|
|
248
|
-
*/
|
|
249
|
-
const POINTER_SENTENCE_PATTERN = new RegExp(
|
|
250
|
-
`Read \\[(?:[^\\]\\n\\\\]|\\\\[\\s\\S])*\\]\\(\\.\\/[^)\\n]*\\)${escapeRegExp(POINTER_SENTENCE_TAIL)}`,
|
|
251
|
-
'g',
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
/** `text` as a literal inside a regular expression. */
|
|
255
|
-
function escapeRegExp(text: string): string {
|
|
256
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* `text` with every pointer sentence THE PLATFORM WROTE aimed at `agentsFile`
|
|
261
|
-
* — or null when it holds none of ours.
|
|
262
|
-
*
|
|
263
|
-
* This is what a SECOND rename needs. The guide's name is a setting an admin
|
|
264
|
-
* may change again: a knowledge base whose `AGENTS.md` was given a sentence
|
|
265
|
-
* pointing at `HEXIS.md` and is then renamed to `GUIDE.md` must have that
|
|
266
|
-
* sentence aimed at the new file, not a second one appended beneath a first
|
|
267
|
-
* that now points at nothing. Asking only whether the NEW name is mentioned
|
|
268
|
-
* cannot see that — the old sentence does not mention it.
|
|
269
|
-
*
|
|
270
|
-
* Returning the text unchanged (rather than null) when the sentence is already
|
|
271
|
-
* right is deliberate: "ours and correct" and "not ours at all" are different
|
|
272
|
-
* answers, and only the caller knows that the second one means "consider
|
|
273
|
-
* appending".
|
|
274
|
-
*/
|
|
275
|
-
export function retargetAgentsFilePointer(text: string, agentsFile: string): string | null {
|
|
276
|
-
let found = false;
|
|
277
|
-
const wanted = agentsFilePointerSentence(agentsFile);
|
|
278
|
-
const updated = text.replace(POINTER_SENTENCE_PATTERN, () => {
|
|
279
|
-
found = true;
|
|
280
|
-
return wanted;
|
|
281
|
-
});
|
|
282
|
-
return found ? updated : null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* `text` as an inline link's LABEL: the characters that would end the label or
|
|
287
|
-
* start emphasis or code inside it, backslash-escaped. Nothing else is touched
|
|
288
|
-
* — a filename is read by people, and `AGENTS\.md` helps no one.
|
|
289
|
-
*/
|
|
290
|
-
function markdownLinkLabel(text: string): string {
|
|
291
|
-
return text.replace(/[\\[\]`*_]/g, (c) => `\\${c}`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* The guide as an inline link's DESTINATION: `./` and the name, percent-encoded.
|
|
296
|
-
*
|
|
297
|
-
* `encodeURI` does most of it (a space, a bracket, and `%` itself, so an
|
|
298
|
-
* already-encoded-looking name is not decoded by a reader). Three more are
|
|
299
|
-
* encoded by hand because `encodeURI` leaves them and each one ENDS the path
|
|
300
|
-
* early: `#` opens a fragment, and `(`/`)` close the destination in
|
|
301
|
-
* CommonMark's bare form. `?` and the rest of the URL-significant set are
|
|
302
|
-
* already refused by {@link validateFilename}.
|
|
303
|
-
*
|
|
304
|
-
* An ordinary name has none of these and comes out exactly as it went in.
|
|
305
|
-
*/
|
|
306
|
-
function agentsFileLinkPath(agentsFile: string): string {
|
|
307
|
-
return `./${encodeURI(agentsFile).replace(/[#()]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}`;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Whether `text` already points at the guide — the ONE question the startup
|
|
312
|
-
* step asks before appending {@link agentsFilePointerSentence} to a customer's
|
|
313
|
-
* own `AGENTS.md`.
|
|
314
|
-
*
|
|
315
|
-
* The plain name is the answer that matters: a mention in the customer's own
|
|
316
|
-
* words, a heading, a link they wrote, all count, and the platform stays out
|
|
317
|
-
* of a file it does not own. The other two spellings are the platform's OWN,
|
|
318
|
-
* and they are here for idempotence: the sentence writes the name escaped in
|
|
319
|
-
* the label and encoded in the destination, so on a punctuated name the file
|
|
320
|
-
* the last boot wrote need not contain the raw name at all. Asking only for
|
|
321
|
-
* that one would append a second copy on the next boot, and a third on the
|
|
322
|
-
* one after — the exact failure this feature exists to prevent.
|
|
323
|
-
*/
|
|
324
|
-
export function mentionsAgentsFile(text: string, agentsFile: string): boolean {
|
|
325
|
-
return (
|
|
326
|
-
text.includes(agentsFile) ||
|
|
327
|
-
text.includes(markdownLinkLabel(agentsFile)) ||
|
|
328
|
-
text.includes(agentsFileLinkPath(agentsFile))
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
205
|
/**
|
|
333
206
|
* What is wrong with one root name, or null. A root is joined onto the repo
|
|
334
207
|
* root and onto `<dir>/.gitkeep`, so a separator or `..` would write outside
|
|
@@ -350,22 +223,16 @@ export function validateKbRootName(name: string): string | null {
|
|
|
350
223
|
}
|
|
351
224
|
|
|
352
225
|
/**
|
|
353
|
-
* What is wrong with the agent guide's
|
|
354
|
-
*
|
|
355
|
-
*
|
|
226
|
+
* What is wrong with the agent guide's name, or null. The rules date from when
|
|
227
|
+
* the guide was written to disk under this name; a saved name still has to be
|
|
228
|
+
* one the read tools can answer to, so they stand:
|
|
356
229
|
*
|
|
357
|
-
* - ONE FILE NAME. The name is
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* rules and stop being readable by everyone.
|
|
364
|
-
* - NOT `CLAUDE.md`. That is the guide's own pre-rename name; knowledge bases
|
|
365
|
-
* seeded before the rename still carry one, and it stays legacy content
|
|
366
|
-
* rather than becoming a second managed file.
|
|
367
|
-
* - NOT ANOTHER PLATFORM FILE. Two platform roles on one path means whichever
|
|
368
|
-
* writer runs last wins, silently.
|
|
230
|
+
* - ONE FILE NAME. The name is read at the repository root and nowhere else.
|
|
231
|
+
* - A MARKDOWN NAME. The guide is markdown that agents read as text.
|
|
232
|
+
* - NOT `CLAUDE.md`. That is the guide's own pre-rename name; a knowledge
|
|
233
|
+
* base seeded before the rename may still carry one of its own.
|
|
234
|
+
* - NOT ANOTHER PLATFORM FILE. A read of `access.md` must answer with the
|
|
235
|
+
* access rules, not the guide.
|
|
369
236
|
* - NOT A ROOT FOLDER'S NAME, compared case-insensitively like the roots are
|
|
370
237
|
* to each other: the workspaces live on case-insensitive filesystems, where
|
|
371
238
|
* a file `Docs.md` and a folder `docs.md` are one entry.
|
|
@@ -530,13 +397,13 @@ export function isDefaultKbLayout(layout: KbLayout): boolean {
|
|
|
530
397
|
}
|
|
531
398
|
|
|
532
399
|
/**
|
|
533
|
-
* Render the layout placeholders a managed
|
|
400
|
+
* Render the layout placeholders a managed text carries —
|
|
534
401
|
* `{{knowledgeBaseDir}}`, `{{skillsDir}}`, `{{pluginsDir}}`, `{{agentsFile}}`
|
|
535
|
-
* — with the names in effect. The
|
|
536
|
-
* written this way so a deployment that renamed its roots
|
|
537
|
-
* guide that names the folders it will actually find, and
|
|
538
|
-
*
|
|
539
|
-
* placeholders passes through unchanged.
|
|
402
|
+
* — with the names in effect. The agent guide's sections and the packaged
|
|
403
|
+
* `.bevelignore` are written this way so a deployment that renamed its roots
|
|
404
|
+
* hands the agent a guide that names the folders it will actually find, and
|
|
405
|
+
* one that gave the guide a name of its own gets a guide naming it. Text
|
|
406
|
+
* without placeholders passes through unchanged.
|
|
540
407
|
*/
|
|
541
408
|
export function renderKbLayoutPlaceholders(text: string, layout: KbLayout): string {
|
|
542
409
|
// Replacer FUNCTIONS: a string replacement would interpret `$&`, `$$` and
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_KB_LAYOUT,
|
|
3
3
|
FIXED_PLATFORM_FILE_NAMES,
|
|
4
|
-
agentsFileOf,
|
|
5
4
|
reservedRootDirNames,
|
|
6
5
|
type KbLayout,
|
|
7
6
|
} from './kb-layout.js';
|
|
@@ -9,28 +8,26 @@ import {
|
|
|
9
8
|
/**
|
|
10
9
|
* The files the platform reads as configuration, not content. `access.md`
|
|
11
10
|
* governs the folder it sits in and `.bevelignore` layers like `.gitignore`,
|
|
12
|
-
* so both count at any depth; `roles.yaml`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* so both count at any depth; `roles.yaml` is read from the repository root
|
|
12
|
+
* only, so a nested file of that name is ordinary content. Moving one changes
|
|
13
|
+
* what the platform enforces, so moves refuse them.
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* The agent guide is NOT among them. It was, while the platform wrote it to
|
|
16
|
+
* the repository root; the backend serves it from code now (core-backend's
|
|
17
|
+
* `modules/agent-guide`), and a root `AGENTS.md` — or a file under whatever
|
|
18
|
+
* name a deployment once gave the guide — is the organisation's own
|
|
19
|
+
* conventions page, which moves and deletes like any other.
|
|
20
|
+
*
|
|
21
|
+
* Still a FUNCTION OF THE LAYOUT, so every gate asks the question the same
|
|
22
|
+
* way it asks the others, and a file the layout makes a platform file again
|
|
23
|
+
* one day needs no caller to change.
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- the layout is the question's shape, kept for every caller (see above)
|
|
26
|
+
export function platformFileNames(_layout: KbLayout): readonly string[] {
|
|
27
|
+
return [...FIXED_PLATFORM_FILE_NAMES];
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
/**
|
|
29
|
-
* The platform file names under the DEFAULT layout — what they were before the
|
|
30
|
-
* guide could be renamed. Kept for callers that want the default answer rather
|
|
31
|
-
* than this deployment's; anything judging a real path asks
|
|
32
|
-
* {@link platformFileNames}, which knows what this deployment called its guide.
|
|
33
|
-
*/
|
|
30
|
+
/** The platform file names under the DEFAULT layout — the same three on every deployment. */
|
|
34
31
|
export const PLATFORM_FILE_NAMES: readonly string[] = Object.freeze(
|
|
35
32
|
platformFileNames(DEFAULT_KB_LAYOUT),
|
|
36
33
|
);
|
|
@@ -41,8 +38,8 @@ const PLATFORM_FILES_AT_ANY_DEPTH = new Set(['access.md', '.bevelignore']);
|
|
|
41
38
|
/**
|
|
42
39
|
* The platform files split by the DEPTH they count at, which is the half of
|
|
43
40
|
* {@link isPlatformFile} that a name alone does not tell you: `access.md` and
|
|
44
|
-
* `.bevelignore` are platform files in any folder, `roles.yaml`
|
|
45
|
-
*
|
|
41
|
+
* `.bevelignore` are platform files in any folder, `roles.yaml` only in the
|
|
42
|
+
* repository root.
|
|
46
43
|
*
|
|
47
44
|
* Exported because the agent-facing rules state that split in prose, and a
|
|
48
45
|
* prose list written by hand drifts from the predicate that actually refuses
|
|
@@ -111,7 +108,7 @@ export function platformFileCreationRefusal(pathOrName: string): string {
|
|
|
111
108
|
/**
|
|
112
109
|
* The sentence an UPLOAD is refused with when one of its paths would land a
|
|
113
110
|
* platform file — a zip carrying an `access.md`, a `.bevelignore`, a
|
|
114
|
-
* `roles.yaml
|
|
111
|
+
* `roles.yaml`, or a single file sent under one of those
|
|
115
112
|
* names.
|
|
116
113
|
*
|
|
117
114
|
* Its own sentence rather than the move's, because the thing being kept out is
|
|
@@ -151,7 +148,7 @@ export function platformFolderRefusal(repoRelativeDir: string): string {
|
|
|
151
148
|
|
|
152
149
|
/**
|
|
153
150
|
* Whether the platform file at `repoRelativePath` sits directly in the
|
|
154
|
-
* repository root — the copy every one of the
|
|
151
|
+
* repository root — the copy every one of the three is read from there, and so
|
|
155
152
|
* never the misplaced one: it is the copy a restore puts back. A nested
|
|
156
153
|
* `access.md` or `.bevelignore` is a platform file too, but it layers on top
|
|
157
154
|
* of the root's rather than standing in for it, which is why moving the
|
|
@@ -165,7 +162,7 @@ export function isRootPlatformFile(repoRelativePath: string, layout: KbLayout):
|
|
|
165
162
|
* The place a misplaced platform file is allowed to be put back, when
|
|
166
163
|
* `repoRelativeDestination` names one, and null when it does not.
|
|
167
164
|
*
|
|
168
|
-
* `roles.yaml`
|
|
165
|
+
* `roles.yaml` is read from the repository root and
|
|
169
166
|
* nowhere else, so their one required location is the root. A nested `.bevelignore` is
|
|
170
167
|
* read too (it layers, see `BevelIgnoreStack`), yet a restore of one lands at
|
|
171
168
|
* the root only — a deliberate narrowing of the exception, not a claim about
|
|
@@ -204,8 +201,8 @@ export function platformRestoreDestination(
|
|
|
204
201
|
* Three things make the shape, and all three are about the move rather than
|
|
205
202
|
* about the source's current standing:
|
|
206
203
|
*
|
|
207
|
-
* - the source is NAMED like a platform file. A nested `roles.yaml
|
|
208
|
-
*
|
|
204
|
+
* - the source is NAMED like a platform file. A nested `roles.yaml` is
|
|
205
|
+
* ordinary content where it sits
|
|
209
206
|
* (`isPlatformFile` says so, and moving it needs no exception), but it is
|
|
210
207
|
* still the copy a restore carries back to the root — judging the shape on
|
|
211
208
|
* `isPlatformFile` would skip the exception for exactly the two files the
|