@bevel-software/platform-shared 0.14.0 → 0.15.1

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.
@@ -1,4 +1,5 @@
1
1
  import { branchSegment } from '../git/branchAuthor.js';
2
+ import { validateFilename } from './filename.js';
2
3
  /**
3
4
  * Top-level layout of the KB repo (inside the `KB_DIR_NAME` clone).
4
5
  *
@@ -7,7 +8,8 @@ import { branchSegment } from '../git/branchAuthor.js';
7
8
  *
8
9
  * <kbDirName>/
9
10
  * ├── KnowledgeBase/ ← all team ontologies live here (the knowledge graph)
10
- * ├── Plugins/ ← one folder per plugin; each holds BOTH skills and tools
11
+ * ├── Skills/ ← shared skills, organised by ownership; plugins LINK to them
12
+ * ├── Plugins/ ← one folder per plugin: manifest, MCP servers, tools, links
11
13
  * ├── Data/ ← agent-produced records; parsed like KnowledgeBase/
12
14
  * ├── Agents/ ← .agent files — agent role configurations (not the graph)
13
15
  * ├── Pipelines/ ← .pipeline files — execution-layer processes (not the graph)
@@ -23,21 +25,44 @@ import { branchSegment } from '../git/branchAuthor.js';
23
25
  *
24
26
  * These names are the single source of truth for both sides of the app:
25
27
  * - Backend: the graph parser discovers ontologies under the
26
- * {@link ONTOLOGY_ROOTS} (`KnowledgeBase/` and `Data/`); `Plugins/`,
28
+ * {@link ontologyRoots} (`KnowledgeBase/` and `Data/`); `Plugins/`,
27
29
  * `Agents/`, `Pipelines/` (and anything else at the root) are ignored by
28
30
  * parsing, validation, and the diagram.
29
31
  * - Frontend: the file tree renders these root folders as distinct
30
32
  * top-level sections.
31
33
  *
32
34
  * Don't hard-code these strings elsewhere — import them from here.
35
+ *
36
+ * CONFIGURABLE, WITH DEFAULTS. The three roots a deployment may rename
37
+ * (`KnowledgeBase/`, `Skills/`, `Plugins/`) are `let` bindings applied by
38
+ * {@link configureKbLayout} — the backend from its deployment settings, the
39
+ * browser from `GET /api/config` — the same live-binding pattern as the branch
40
+ * model in `git/protected.ts`. Unlike the branch model they carry defaults, so
41
+ * nothing has to wait for configuration; but the same rule applies: read them
42
+ * inside a function body, never capture one at module scope.
33
43
  */
34
44
  /** Folder under the repo root that contains all team ontologies. */
35
- export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
45
+ export let KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
46
+ /**
47
+ * Folder under the repo root that holds SHARED skills, organised by ownership:
48
+ *
49
+ * Skills/<scope>/…/<skill>/SKILL.md a skill, at any depth
50
+ * Skills/<scope>/access.md who owns / may read the scope
51
+ *
52
+ * A skill's readability comes from ITS OWN path walk — the scope folders'
53
+ * `access.md` files — never from the plugins that link it. Plugins point at
54
+ * skills here by path (see `HEXIS_LINKED_SKILLS_KEY`), so one definition can
55
+ * ship in several plugins, and a skill in no plugin at all is a normal state.
56
+ * Inline skills under `Plugins/<Plugin>/skills/` remain supported (personal
57
+ * folders, legacy layouts); the catalog is the union of both trees.
58
+ */
59
+ export let SKILLS_DIR = 'Skills';
36
60
  /**
37
61
  * Folder under the repo root that holds the plugins.
38
62
  *
39
- * Plugins/<Plugin>/plugin.json the Agent Plugins manifest
40
- * Plugins/<Plugin>/skills/<skill>/SKILL.md a skill
63
+ * Plugins/<Plugin>/plugin.json the Agent Plugins manifest; its
64
+ * hexis extension lists LINKED skills
65
+ * Plugins/<Plugin>/skills/<skill>/SKILL.md an inline skill
41
66
  * Plugins/<Plugin>/mcp.json MCP servers
42
67
  * Plugins/<Plugin>/software.bevel.hexis/tools/ http + inline `.tool` manuals
43
68
  * Plugins/<Plugin>/access.md who can read/write the plugin
@@ -56,10 +81,11 @@ export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
56
81
  * `inline` types the spec has no slot for. `mcp`-type manuals are emitted as
57
82
  * real `mcp.json` entries instead, so the portable half stays portable.
58
83
  *
59
- * Skills and tools live TOGETHER in one plugin because they share a single
60
- * access boundary: a tool a plugin cannot read is a skill that plugin cannot
61
- * run, so splitting them across two roots meant maintaining the same permission
62
- * twice and letting them drift.
84
+ * A plugin's own `access.md` governs what the plugin FOLDER holds: the
85
+ * manifest, the MCP servers, the tools, and any inline skills. Shared skills
86
+ * under `Skills/` are governed by their own scope and are made visible to a
87
+ * plugin's members by granting the plugin's principal (`plugin/<Name>/read`)
88
+ * on the skill — ownership decides, the plugin is a view.
63
89
  *
64
90
  * A plugin is not a registry of unique names — it is a folder. The same
65
91
  * integration may exist in several plugins as separate files (`Everyone/…/
@@ -71,7 +97,98 @@ export const KNOWLEDGE_BASE_DIR = 'KnowledgeBase';
71
97
  * display casing. The lowercase slug the spec does constrain lives in the
72
98
  * manifest's `name` field.
73
99
  */
74
- export const PLUGINS_DIR = 'Plugins';
100
+ export let PLUGINS_DIR = 'Plugins';
101
+ /** The layout a deployment gets when it names nothing. */
102
+ export const DEFAULT_KB_LAYOUT = Object.freeze({
103
+ knowledgeBaseDir: 'KnowledgeBase',
104
+ skillsDir: 'Skills',
105
+ pluginsDir: 'Plugins',
106
+ });
107
+ /**
108
+ * What is wrong with one root name, or null. A root is joined onto the repo
109
+ * root and onto `<dir>/.gitkeep`, so a separator or `..` would write outside
110
+ * the repository; a dot-prefixed name would be skipped by every scanner that
111
+ * treats dot-entries as bookkeeping; `.git` in any case would corrupt the clone.
112
+ */
113
+ export function validateKbRootName(name) {
114
+ const v = name.trim();
115
+ if (!v)
116
+ return 'A folder name is required.';
117
+ if (v.includes('/') || v.includes('\\'))
118
+ return 'Use a single folder name — no slashes.';
119
+ // The ONE rule for what a path segment may be called — the same one every
120
+ // file and folder made through the platform passes (reserved Windows
121
+ // names, trailing dots, forbidden characters, length) — plus what a ROOT
122
+ // must not be: dot-prefixed, which every scanner skips as bookkeeping.
123
+ const asName = validateFilename(v);
124
+ if (asName)
125
+ return asName;
126
+ if (v.startsWith('.'))
127
+ return 'The name can\'t start with a dot.';
128
+ return null;
129
+ }
130
+ /**
131
+ * What is wrong with a layout, or null — the same rule {@link configureKbLayout}
132
+ * enforces, without applying anything. Separate so the setup screen can judge a
133
+ * proposed layout before it is saved. The three names must differ, compared
134
+ * case-insensitively: the workspaces live on case-insensitive filesystems too,
135
+ * where `Skills` and `skills` are one folder.
136
+ */
137
+ export function validateKbLayout(layout) {
138
+ for (const [label, value] of [
139
+ ['knowledge base', layout.knowledgeBaseDir],
140
+ ['skills', layout.skillsDir],
141
+ ['plugins', layout.pluginsDir],
142
+ ]) {
143
+ const problem = validateKbRootName(value ?? '');
144
+ if (problem)
145
+ return `The ${label} folder: ${problem}`;
146
+ }
147
+ const names = [layout.knowledgeBaseDir, layout.skillsDir, layout.pluginsDir].map((n) => n.trim().toLowerCase());
148
+ if (new Set(names).size !== names.length) {
149
+ return 'The knowledge base, skills and plugins folders must have three different names.';
150
+ }
151
+ // The fixed reserved roots are taken too: naming the skills folder `Data`
152
+ // would give one directory two reserved roles.
153
+ const fixed = [DATA_DIR, AGENTS_DIR, PIPELINES_DIR].map((n) => n.toLowerCase());
154
+ const clash = names.find((n) => fixed.includes(n));
155
+ if (clash)
156
+ return `"${clash}" is a reserved folder name (${[DATA_DIR, AGENTS_DIR, PIPELINES_DIR].join(', ')}).`;
157
+ return null;
158
+ }
159
+ /**
160
+ * Apply the layout. Called once during boot on each side; throws on an invalid
161
+ * one so a bad deployment setting fails beside the rest of the wiring rather
162
+ * than scattering a half-renamed tree. Applying the defaults is a no-op.
163
+ */
164
+ export function configureKbLayout(layout) {
165
+ const problem = validateKbLayout(layout);
166
+ if (problem)
167
+ throw new Error(problem);
168
+ KNOWLEDGE_BASE_DIR = layout.knowledgeBaseDir.trim();
169
+ SKILLS_DIR = layout.skillsDir.trim();
170
+ PLUGINS_DIR = layout.pluginsDir.trim();
171
+ }
172
+ /** The layout currently in effect. */
173
+ export function currentKbLayout() {
174
+ return { knowledgeBaseDir: KNOWLEDGE_BASE_DIR, skillsDir: SKILLS_DIR, pluginsDir: PLUGINS_DIR };
175
+ }
176
+ /**
177
+ * Render the layout placeholders a managed template carries —
178
+ * `{{knowledgeBaseDir}}`, `{{skillsDir}}`, `{{pluginsDir}}` — with the
179
+ * names in effect. The packaged `AGENTS.md` and `.bevelignore` are written
180
+ * this way so a deployment that renamed its roots hands the agent a guide
181
+ * that names the folders it will actually find. Text without placeholders
182
+ * passes through unchanged.
183
+ */
184
+ export function renderKbLayoutPlaceholders(text, layout = currentKbLayout()) {
185
+ // Replacer FUNCTIONS: a string replacement would interpret `$&`, `$$` and
186
+ // friends inside a folder name, and `$` is a legal character in one.
187
+ return text
188
+ .replaceAll('{{knowledgeBaseDir}}', () => layout.knowledgeBaseDir)
189
+ .replaceAll('{{skillsDir}}', () => layout.skillsDir)
190
+ .replaceAll('{{pluginsDir}}', () => layout.pluginsDir);
191
+ }
75
192
  /**
76
193
  * The pre-rename name of {@link PLUGINS_DIR}. Referenced ONLY by the migration
77
194
  * that renames it — every other consumer should be reading the new name, and a
@@ -94,6 +211,105 @@ export const PLUGIN_SKILLS_DIR = 'skills';
94
211
  export const HEXIS_EXTENSION_NS = 'software.bevel.hexis';
95
212
  /** UTCP manuals whose `http`/`inline` types the spec cannot express. */
96
213
  export const HEXIS_TOOLS_DIR = `${HEXIS_EXTENSION_NS}/tools`;
214
+ /**
215
+ * The manifest key under which a plugin LINKS shared skills:
216
+ *
217
+ * plugin.json → extensions["software.bevel.hexis"].skills: [
218
+ * "Skills/Engineering/deploy", ← one skill folder
219
+ * "Skills/Sales" ← a folder of skills: every skill beneath
220
+ * ]
221
+ *
222
+ * Entries are repo-root-relative folder paths. A plugin's effective skill set
223
+ * is its inline `skills/` folder PLUS everything these roots resolve to. The
224
+ * spec reserves `extensions` for exactly this kind of client-specific data, so
225
+ * a conformant client that ignores it still gets a valid manifest; the
226
+ * compiled distribution copies the linked skills in for it.
227
+ *
228
+ * Linking is a reference, not a grant: a member of the plugin can read a
229
+ * linked skill only because the skill's own access rules name the plugin's
230
+ * principal (`plugin/<Name>/read`). The link service writes both together.
231
+ */
232
+ export const HEXIS_LINKED_SKILLS_KEY = 'skills';
233
+ /**
234
+ * Normalise a linked-skill root, or null when it cannot be one: a
235
+ * repo-root-relative POSIX folder path with no `..`, no leading slash, no
236
+ * backslashes and no empty segments. Trailing slashes are dropped.
237
+ */
238
+ export function normalizeSkillRoot(raw) {
239
+ if (typeof raw !== 'string')
240
+ return null;
241
+ const trimmed = raw.trim();
242
+ if (!trimmed || trimmed.includes('\\') || trimmed.startsWith('/'))
243
+ return null;
244
+ const segments = trimmed.split('/');
245
+ // Only TRAILING slashes are forgiven; an empty segment anywhere else
246
+ // (`Skills//deploy`) is a malformed path, not a spelling of a valid one.
247
+ while (segments.length > 0 && segments[segments.length - 1] === '')
248
+ segments.pop();
249
+ if (segments.length === 0)
250
+ return null;
251
+ if (segments.some((s) => s === '' || s === '.' || s === '..'))
252
+ return null;
253
+ return segments.join('/');
254
+ }
255
+ /**
256
+ * The linked-skill roots a parsed manifest declares — invalid entries are
257
+ * dropped, duplicates collapsed, order kept. A manifest with no extension
258
+ * block links nothing.
259
+ */
260
+ export function linkedSkillRoots(manifest) {
261
+ if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
262
+ return [];
263
+ const ext = manifest.extensions;
264
+ if (typeof ext !== 'object' || ext === null)
265
+ return [];
266
+ const ns = ext[HEXIS_EXTENSION_NS];
267
+ if (typeof ns !== 'object' || ns === null)
268
+ return [];
269
+ const raw = ns[HEXIS_LINKED_SKILLS_KEY];
270
+ if (!Array.isArray(raw))
271
+ return [];
272
+ const out = [];
273
+ for (const entry of raw) {
274
+ const root = normalizeSkillRoot(typeof entry === 'string' ? entry : '');
275
+ if (root !== null && !out.includes(root))
276
+ out.push(root);
277
+ }
278
+ return out;
279
+ }
280
+ /**
281
+ * The manifest with its linked-skill roots REPLACED by `roots`, every other
282
+ * byte of the object preserved (the MCP extension block beside it, the
283
+ * portable fields above it). An empty list removes the key rather than
284
+ * leaving `skills: []` behind.
285
+ */
286
+ export function withLinkedSkillRoots(manifest, roots) {
287
+ const extensions = typeof manifest.extensions === 'object' && manifest.extensions !== null && !Array.isArray(manifest.extensions)
288
+ ? { ...manifest.extensions }
289
+ : {};
290
+ const current = extensions[HEXIS_EXTENSION_NS];
291
+ const ns = typeof current === 'object' && current !== null && !Array.isArray(current)
292
+ ? { ...current }
293
+ : {};
294
+ if (roots.length > 0)
295
+ ns[HEXIS_LINKED_SKILLS_KEY] = [...roots];
296
+ else
297
+ delete ns[HEXIS_LINKED_SKILLS_KEY];
298
+ if (Object.keys(ns).length > 0)
299
+ extensions[HEXIS_EXTENSION_NS] = ns;
300
+ else
301
+ delete extensions[HEXIS_EXTENSION_NS];
302
+ const out = { ...manifest };
303
+ if (Object.keys(extensions).length > 0)
304
+ out.extensions = extensions;
305
+ else
306
+ delete out.extensions;
307
+ return out;
308
+ }
309
+ /** Whether `skillPath` (a skill folder) falls under `root` (a skill folder or a folder of skills). */
310
+ export function skillUnderRoot(skillPath, root) {
311
+ return skillPath === root || skillPath.startsWith(`${root}/`);
312
+ }
97
313
  /**
98
314
  * The manifest `name` for a plugin folder: lowercased, anything outside
99
315
  * `[a-z0-9.-]` folded to `-`, runs collapsed, ends trimmed to alphanumerics.
@@ -123,19 +339,50 @@ export const AGENT_PLUGINS_SCHEMA_VERSION = '1.0.0';
123
339
  export const PLUGIN_MANIFEST_SCHEMA = `https://agent-plugins.org/schemas/${AGENT_PLUGINS_SCHEMA_VERSION}/plugin.schema.json`;
124
340
  export const PLUGIN_MCP_SCHEMA = `https://agent-plugins.org/schemas/${AGENT_PLUGINS_SCHEMA_VERSION}/mcp.schema.json`;
125
341
  /**
126
- * A minimal, valid `plugin.json` for a plugin folder.
127
- *
128
- * Deliberately only the two required fields. `version`, `license` and the rest
129
- * are optional metadata about a DISTRIBUTED package, and inventing values for a
130
- * folder someone just made in the app would be asserting things nobody said —
131
- * a plugin here is a place a team keeps skills, not something published.
132
- *
133
- * The display name is the folder, which is why nothing here carries one: the
134
- * manifest's `name` is constrained to a lowercase slug, and the field set is
135
- * closed, so there is no conformant home for "Sales" other than an extension.
342
+ * The Agent Plugins `name`: a kebab-case identifier — lowercase letters and
343
+ * digits in hyphen-separated runs, nothing else. It is the plugin's IDENTITY:
344
+ * what the marketplace publishes it as, what the access principals are
345
+ * spelled from (`plugin/<name>/<verb>`), what the catalog and the URLs key
346
+ * on. `pluginManifestName` folds any spelling into one of these.
347
+ */
348
+ export const PLUGIN_IDENTIFIER_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
349
+ export function isPluginIdentifier(name) {
350
+ return typeof name === 'string' && PLUGIN_IDENTIFIER_RE.test(name);
351
+ }
352
+ /**
353
+ * The identity of a plugin folder: the manifest's `name` when it IS an
354
+ * identifier, else the folder name folded into one. A manifest naming
355
+ * something that cannot be an identifier is not silently reinterpreted; the
356
+ * folder stands in, and discovery says so.
357
+ */
358
+ export function pluginIdentityOf(manifest, folderName) {
359
+ const declared = manifest?.name;
360
+ return isPluginIdentifier(declared) ? declared : pluginManifestName(folderName);
361
+ }
362
+ /**
363
+ * What a person sees the plugin called: the manifest's `displayName` (the
364
+ * vendor field Claude Code shows in its picker; any casing, spaces allowed),
365
+ * else the folder name — which is what every plugin made before this field
366
+ * existed was called, so nothing renames itself on upgrade.
367
+ */
368
+ export function pluginDisplayNameOf(manifest, folderName) {
369
+ const declared = manifest?.displayName;
370
+ return typeof declared === 'string' && declared.trim() ? declared.trim() : folderName;
371
+ }
372
+ /**
373
+ * A minimal, valid `plugin.json` for a plugin folder: the identifier the
374
+ * folder name folds into, and — when the folder is spelled differently — the
375
+ * folder's spelling as `displayName`, so a client's picker shows "Sales
376
+ * Team" for `sales-team`. Nothing else: `version`, `license` and the rest
377
+ * are metadata about a DISTRIBUTED package, and inventing values for a
378
+ * folder someone just made in the app would be asserting things nobody said.
136
379
  */
137
380
  export function renderPluginManifest(folderName) {
138
- return `${JSON.stringify({ $schema: PLUGIN_MANIFEST_SCHEMA, name: pluginManifestName(folderName) }, null, 2)}\n`;
381
+ const name = pluginManifestName(folderName);
382
+ const manifest = { $schema: PLUGIN_MANIFEST_SCHEMA, name };
383
+ if (folderName !== name)
384
+ manifest.displayName = folderName;
385
+ return `${JSON.stringify(manifest, null, 2)}\n`;
139
386
  }
140
387
  /**
141
388
  * The reserved name prefix marking a personal folder under `Plugins/` —
@@ -164,6 +411,17 @@ export function personalPluginFolderName(userId) {
164
411
  export function isPersonalPluginFolder(folderName) {
165
412
  return folderName.startsWith(PERSONAL_PLUGIN_PREFIX);
166
413
  }
414
+ /**
415
+ * THE structural rule for a personal shelf: a repo-relative folder that is a
416
+ * DIRECT child of the plugins root and carries the personal prefix. A deeper
417
+ * folder so named is just a name, and a plugin whose manifest name happens
418
+ * to start with the prefix is a plugin — discovery, the principal picker and
419
+ * the item pages all ask this one question of the FOLDER.
420
+ */
421
+ export function isPersonalPluginDir(repoRelDir) {
422
+ const segments = repoRelDir.split('/').filter(Boolean);
423
+ return segments.length === 2 && segments[0] === PLUGINS_DIR && isPersonalPluginFolder(segments[1]);
424
+ }
167
425
  /**
168
426
  * The plugin a repo-root-relative path belongs to, or `null` for content that
169
427
  * sits outside any plugin.
@@ -199,8 +457,19 @@ export const PIPELINES_DIR = 'Pipelines';
199
457
  /**
200
458
  * The roots whose subfolders are discovered as ontologies by the graph parser
201
459
  * (each subfolder with both `NodeTypes/` and `Knowledge/` is an ontology).
460
+ * A function, not a constant: `KNOWLEDGE_BASE_DIR` is configurable, and a
461
+ * module-scope array would snapshot the default before configuration.
462
+ */
463
+ export function ontologyRoots() {
464
+ return [KNOWLEDGE_BASE_DIR, DATA_DIR];
465
+ }
466
+ /**
467
+ * Every reserved root name, as currently configured — the set the file tree
468
+ * renders as its own sections rather than folding into Knowledge.
202
469
  */
203
- export const ONTOLOGY_ROOTS = [KNOWLEDGE_BASE_DIR, DATA_DIR];
470
+ export function reservedRootDirNames() {
471
+ return new Set([KNOWLEDGE_BASE_DIR, SKILLS_DIR, PLUGINS_DIR, DATA_DIR, AGENTS_DIR, PIPELINES_DIR]);
472
+ }
204
473
  /** The `Knowledge/` marker subfolder of an ontology (holds the graph nodes). */
205
474
  export const KNOWLEDGE_DIR = 'Knowledge';
206
475
  /** The `NodeTypes/` marker subfolder of an ontology (holds the type definitions). */
@@ -1 +1 @@
1
- {"version":3,"file":"kb-layout.js","sourceRoot":"","sources":["../../src/workspace/kb-layout.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"kb-layout.js","sourceRoot":"","sources":["../../src/workspace/kb-layout.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bevel-software/platform-shared",
3
- "version": "0.14.0",
3
+ "version": "0.15.1",
4
4
  "description": "Shared types and pure domain utilities of the Bevel core platform (auth, workspace, git/workflow contracts, branch registry).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/git/types.ts CHANGED
@@ -68,9 +68,21 @@ export interface ValidationReport {
68
68
  rawOutput: string;
69
69
  }
70
70
 
71
+ /** What `IGitService.syncFromRemote` observed under its one hold of the clone. */
72
+ export interface RemoteSyncPullResult {
73
+ /** HEAD before the pull; null when the clone had no commits yet. */
74
+ before: string | null;
75
+ /** HEAD after the pull; null only when origin is still empty too. */
76
+ after: string | null;
77
+ /** Whether the working tree's CONTENT differs — tree ids, not commit ids. */
78
+ treeChanged: boolean;
79
+ /** Repo-relative paths whose content changed; empty unless `treeChanged`. */
80
+ changedPaths: string[];
81
+ }
82
+
71
83
  export interface IGitService {
72
84
  status(workspaceId: string): Promise<WorkingTreeStatus>;
73
- listBranches(workspaceId: string, opts?: { freshFetch?: boolean }): Promise<BranchInfo[]>;
85
+ listBranches(workspaceId: string, opts?: { freshFetch?: boolean; strictFetch?: boolean }): Promise<BranchInfo[]>;
74
86
  createBranch(
75
87
  workspaceId: string,
76
88
  name: string,
@@ -129,7 +141,39 @@ export interface IGitService {
129
141
  },
130
142
  ): Promise<void>;
131
143
  fetch(workspaceId: string): Promise<void>;
132
- pull(workspaceId: string): Promise<void>;
144
+ /**
145
+ * `treeChanged` is whether the pull left the working tree holding different
146
+ * CONTENT than before the call — tree ids compared, not commit ids, so a
147
+ * pull that only moves HEAD across content-identical commits (an empty
148
+ * commit, a rebase that replays to the same result) reports false. Only the
149
+ * pull itself can answer that (it holds the workspace mutex across the
150
+ * rebase; any before/after probe a caller ran around it would race), and
151
+ * callers that announce "this tree changed" to the rest of the process need
152
+ * the distinction: an "already up to date" pull that broadcast anyway would
153
+ * drop every catalog cache and reload every attached browser for nothing.
154
+ */
155
+ pull(workspaceId: string): Promise<{ treeChanged: boolean }>;
156
+ /**
157
+ * The remote sync's pull, observed as ONE serialized operation: where HEAD
158
+ * was, the pull, where HEAD is, and which repo-relative paths changed
159
+ * (rename-aware: both ends). `pull` bracketed by separate reads would let a
160
+ * concurrent save land between them and be announced as the sync's own.
161
+ *
162
+ * Tolerant of an unborn HEAD (a clone of an empty upstream): `before` is
163
+ * null, and paths are diffed against the empty tree. Throws the typed
164
+ * pull-conflict error like `pull`, and a typed "remote branch gone" error
165
+ * when origin no longer has the branch — including for an unborn clone,
166
+ * once origin has any branch at all. The one exception: an unborn clone
167
+ * against an origin with NO branches (a fresh deployment nobody has pushed
168
+ * to) resolves to `after: null`, since there is nothing to sync and nothing
169
+ * stale.
170
+ */
171
+ syncFromRemote(workspaceId: string): Promise<RemoteSyncPullResult>;
172
+ /**
173
+ * Whether origin still has `branch` right now (`ls-remote`). Used to
174
+ * revalidate that a clone is still stale before it is retired.
175
+ */
176
+ remoteBranchExists(workspaceId: string, branch: string): Promise<boolean>;
133
177
  diffStat(workspaceId: string, base?: string): Promise<string[]>;
134
178
  /**
135
179
  * Paths in the working tree that the next commit would include — the set
@@ -132,6 +132,13 @@ export interface GitSyncFailedEvent {
132
132
  branch: string;
133
133
  /** Sanitised git error — safe to display, tokens already redacted. */
134
134
  reason: string;
135
+ /**
136
+ * Present when the failure is a REMOTE-SYNC CONFLICT (`POST /api/sync`
137
+ * found Hexis-side commits that contradict what landed on the host): the
138
+ * repo-relative files a person has to reconcile. Unlike a failing push,
139
+ * this is the author's to act on, so the banner offers to open them.
140
+ */
141
+ conflictedPaths?: string[];
135
142
  }
136
143
 
137
144
  /**
@@ -21,6 +21,7 @@ import type { AuthUser } from '../auth/types.js';
21
21
  import type {
22
22
  AcquireLockResult,
23
23
  Branch,
24
+ BranchSyncOutcome,
24
25
  BranchWorkspaceStatus,
25
26
  CancelChangeRequestResult,
26
27
  Change,
@@ -40,11 +41,18 @@ import type {
40
41
  export interface IWorkflowService {
41
42
  // ── Branches ──────────────────────────────────────────────────────────────
42
43
 
43
- listBranches(workspaceId: string, opts?: { freshFetch?: boolean }): Promise<Branch[]>;
44
+ /**
45
+ * `freshFetch` bypasses the fetch TTL; `strictFetch` makes a failed fetch
46
+ * throw instead of serving the stale refs — for a caller that uses the
47
+ * list to prove a branch ABSENT (a stale list proves nothing).
48
+ */
49
+ listBranches(workspaceId: string, opts?: { freshFetch?: boolean; strictFetch?: boolean }): Promise<Branch[]>;
44
50
  /**
45
51
  * Create a new unprotected branch. Protected branches cannot be created
46
52
  * via the workflow — the protected set is bootstrapped from the KB repo's
47
- * initial state and never grown at runtime.
53
+ * initial state and never grown at runtime. Holds the branch-lifecycle
54
+ * lock for `name`, so creation is serialised with the branch's deletion
55
+ * and with the retirement of a stale clone under that name.
48
56
  */
49
57
  createBranch(workspaceId: string, name: string, fromBase?: string): Promise<Branch>;
50
58
  /**
@@ -101,6 +109,31 @@ export interface IWorkflowService {
101
109
  * `user` when provided (falls back to the recovery bot).
102
110
  */
103
111
  updateFromRemote(workspaceId: string, user?: AuthUser): Promise<void>;
112
+ /**
113
+ * The remote-sync step for ONE branch's clone, driven by a git host's
114
+ * webhook or a pipeline rather than by a person's save. Pulls with the same
115
+ * rebase-and-autostash `updateFromRemote` uses and, when HEAD moved,
116
+ * announces the new tree (`fs-tree-changed`, one `file-changed` per path)
117
+ * so open browsers, agents and the catalogues see it at once.
118
+ *
119
+ * On a conflict it does what `updateFromRemote` does — the divergence goes
120
+ * to the same background recovery ladder (attributed to the recovery bot,
121
+ * since no person triggered the sync) — and, because that is not resolved
122
+ * at request time, reports it as a `conflict` outcome naming the files, so
123
+ * the caller can fail and a person can step in if recovery does not clear
124
+ * it. Never throws for a per-branch failure; every failure is an outcome.
125
+ */
126
+ syncWorkspaceFromRemote(workspaceId: string): Promise<BranchSyncOutcome>;
127
+ /**
128
+ * Retire the clone of a branch the host has deleted (a `remote-gone`
129
+ * outcome). Runs under the branch-lifecycle lock — the one `createBranch`
130
+ * and `deleteBranch` hold, so no Hexis operation can recreate the branch
131
+ * while the clone is examined and removed — revalidates against origin
132
+ * first, and backs off while a clone of the branch is being bootstrapped.
133
+ * A branch recreated in the meantime therefore keeps its clone. Returns
134
+ * whether the clone was removed.
135
+ */
136
+ retireRemoteGoneClone(workspaceId: string): Promise<boolean>;
104
137
  /**
105
138
  * Hard-reset the workspace's checked-out branch to `origin/<branch>` (fetch
106
139
  * first), discarding any local divergence. Break-glass primitive for
@@ -163,3 +163,30 @@ export interface OpenChangeRequestInput {
163
163
  export type MergeChangeRequestOutcome =
164
164
  | { kind: 'merged'; result: MergeChangeRequestResult }
165
165
  | { kind: 'conflicts-need-resolution'; conflictedPaths: string[] };
166
+
167
+ /**
168
+ * What a remote sync did to one branch's clone. One entry per branch in the
169
+ * `POST /api/sync` response, so a pipeline can read exactly which branch
170
+ * moved, which was already current, and which needs a person.
171
+ *
172
+ * - `updated` — the clone moved from `from` (null when it had no commits
173
+ * yet) to `to`.
174
+ * - `up-to-date` — origin had nothing new; `to` is the unchanged HEAD.
175
+ * - `not-cloned` — Hexis has no clone of this branch, so there is nothing
176
+ * to refresh (the first visit clones it fresh).
177
+ * - `remote-gone` — the branch no longer exists on the host; the stale clone
178
+ * is removed. Not a failure: a deleted branch has nothing
179
+ * to sync.
180
+ * - `conflict` — Hexis-side commits contradict what landed on the host.
181
+ * The clone is untouched; `error` is the message to show
182
+ * and `conflictedPaths` the files a person must reconcile.
183
+ * - `error` — the pull failed for another reason (origin unreachable,
184
+ * credential refused); `error` is the sanitised message.
185
+ */
186
+ export type BranchSyncOutcome =
187
+ | { branch: string; outcome: 'updated'; from: string | null; to: string }
188
+ | { branch: string; outcome: 'up-to-date'; to: string }
189
+ | { branch: string; outcome: 'not-cloned' }
190
+ | { branch: string; outcome: 'remote-gone' }
191
+ | { branch: string; outcome: 'conflict'; conflictedPaths: string[]; error: string }
192
+ | { branch: string; outcome: 'error'; error: string };
@@ -23,9 +23,13 @@ const WINDOWS_RESERVED_NAMES = new Set([
23
23
 
24
24
  /** Characters Windows forbids in any filename component. `/` is the path
25
25
  * separator on Unix, included for the same reason. `\` is also a path
26
- * separator on Windows. Control chars (0x00–0x1F) are rejected separately. */
26
+ * separator on Windows. Control characters are rejected with them: NUL and
27
+ * 0x01–0x1F because Windows refuses them, and DEL (0x7F) — which every
28
+ * filesystem would write — as a matter of hygiene: a name carrying a
29
+ * character nobody can see or type is not a name people can share, and the
30
+ * knowledge-base root names are validated by this same rule. */
27
31
  // eslint-disable-next-line no-control-regex
28
- const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1F]/;
32
+ const FORBIDDEN_CHARS = /[<>:"/\\|?*\x00-\x1F\x7F]/;
29
33
 
30
34
  /** Per-component byte limit: NTFS = 255 UTF-16 units, ext4 = 255 bytes,
31
35
  * APFS = 255 UTF-8 bytes. Use UTF-8 bytes — the strictest of the three. */
@@ -45,7 +49,7 @@ export function validateFilename(name: string): string | null {
45
49
  if (name === '.' || name === '..') return 'Name cannot be "." or ".."';
46
50
 
47
51
  if (FORBIDDEN_CHARS.test(name)) {
48
- return 'Name cannot contain any of these characters: < > : " / \\ | ? *';
52
+ return 'Name cannot contain control characters or any of: < > : " / \\ | ? *';
49
53
  }
50
54
 
51
55
  // Windows trims trailing dots and spaces silently — a name ending in either