@f5-sales-demo/xcsh 19.93.0 → 19.94.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.
- package/package.json +8 -8
- package/src/internal-urls/build-info-runtime.ts +8 -4
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/console-catalog.generated.ts +2 -2
- package/src/internal-urls/fleet-resolve.ts +571 -0
- package/src/internal-urls/xcsh-protocol.ts +27 -4
- package/src/prompts/system/system-prompt.md +34 -8
- package/src/sdk.ts +4 -0
- package/src/services/xcsh-knowledge.ts +19 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.
|
|
4
|
+
"version": "19.94.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
58
58
|
"@mozilla/readability": "^0.6",
|
|
59
|
-
"@f5-sales-demo/xcsh-stats": "19.
|
|
60
|
-
"@f5-sales-demo/pi-agent-core": "19.
|
|
61
|
-
"@f5-sales-demo/pi-ai": "19.
|
|
62
|
-
"@f5-sales-demo/pi-natives": "19.
|
|
63
|
-
"@f5-sales-demo/pi-resource-management": "19.
|
|
64
|
-
"@f5-sales-demo/pi-tui": "19.
|
|
65
|
-
"@f5-sales-demo/pi-utils": "19.
|
|
59
|
+
"@f5-sales-demo/xcsh-stats": "19.94.1",
|
|
60
|
+
"@f5-sales-demo/pi-agent-core": "19.94.1",
|
|
61
|
+
"@f5-sales-demo/pi-ai": "19.94.1",
|
|
62
|
+
"@f5-sales-demo/pi-natives": "19.94.1",
|
|
63
|
+
"@f5-sales-demo/pi-resource-management": "19.94.1",
|
|
64
|
+
"@f5-sales-demo/pi-tui": "19.94.1",
|
|
65
|
+
"@f5-sales-demo/pi-utils": "19.94.1",
|
|
66
66
|
"@sinclair/typebox": "^0.34",
|
|
67
67
|
"@xterm/headless": "^6.0",
|
|
68
68
|
"ajv": "^8.20",
|
|
@@ -220,6 +220,7 @@ export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus |
|
|
|
220
220
|
'2. For recent changes / "what\'s new", read `xcsh://changes` — it lists merged PRs live and flags',
|
|
221
221
|
" what shipped after your build (it falls back to `gh pr list` / `git log` when gh is unavailable).",
|
|
222
222
|
' A fix may already be on `main`. For "where is X implemented?", read `xcsh://source`.',
|
|
223
|
+
" For the class of the repository you are working in and what you may author there, read `xcsh://fleet`.",
|
|
223
224
|
"3. If behavior contradicts `xcsh://…` docs, read the actual source under the repo above to determine",
|
|
224
225
|
" whether the binary is wrong or the doc is stale.",
|
|
225
226
|
"4. Classify the report as one of: **bug**, **feature**, **docs-drift**, or **config/usage**.",
|
|
@@ -242,10 +243,13 @@ export function renderAboutDoc(info: RuntimeBuildInfo, context: ContextStatus |
|
|
|
242
243
|
"",
|
|
243
244
|
"The improvement workflow is always: open an issue on the repo, then a PR. The user receives changes only after a new release is built and they upgrade. Do not claim a change is live until the commit above reflects it.",
|
|
244
245
|
"",
|
|
245
|
-
"You are a GitHub
|
|
246
|
-
"reproduction, the Terraform/manifests/scripts/docs, and the PR description
|
|
247
|
-
"
|
|
248
|
-
"
|
|
246
|
+
"You are a network-engineer assistant operating through GitHub, not a coding assistant: your job is",
|
|
247
|
+
"to author the issue, the reproduction, the Terraform/manifests/scripts/docs, and the PR description.",
|
|
248
|
+
"Whether **implementing** the change is yours depends on the repository's class — read `xcsh://fleet`.",
|
|
249
|
+
"xcsh itself is classified `developer`, so feature code here is delegated to a dedicated coding",
|
|
250
|
+
"harness (Claude Code / Codex) with its own dev environment; the same holds for the other `developer`",
|
|
251
|
+
"repositories such as `f5-sales-demo/marketplace` and `f5-sales-demo/api-specs-enriched`. In a",
|
|
252
|
+
"`content` repository you author directly.",
|
|
249
253
|
"To file well: clone the relevant repo, reproduce the behavior first, and follow `CONTRIBUTING.md` —",
|
|
250
254
|
"TDD, evidence required, no unverified claims.",
|
|
251
255
|
"",
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.94.1",
|
|
21
|
+
"commit": "960cef12ede26aa043de205b6faf4a8becc0ea9c",
|
|
22
|
+
"shortCommit": "960cef1",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.94.1",
|
|
25
|
+
"commitDate": "2026-07-26T10:03:06Z",
|
|
26
|
+
"buildDate": "2026-07-26T10:26:20.376Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/960cef12ede26aa043de205b6faf4a8becc0ea9c",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.94.1"
|
|
33
33
|
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { ConsoleCatalogData } from "./console-catalog-types";
|
|
4
4
|
|
|
5
|
-
export const CONSOLE_CATALOG_VERSION = "
|
|
5
|
+
export const CONSOLE_CATALOG_VERSION = "d0c5c7c4ceba9b377124550ebf66b9a13717f5ff";
|
|
6
6
|
|
|
7
7
|
export const CONSOLE_CATALOG_DATA: ConsoleCatalogData = {
|
|
8
|
-
version: "
|
|
8
|
+
version: "d0c5c7c4ceba9b377124550ebf66b9a13717f5ff",
|
|
9
9
|
workflows: {
|
|
10
10
|
"address-allocator/create":
|
|
11
11
|
'schema: urn:xcsh:console:workflow:v1\nid: address-allocator-create\nlabel: Create IP Address Allocators\nresource: address-allocator\noperation: create\npreconditions:\n - user_logged_in\n - namespace_selected\n - "role_minimum: admin"\nparams:\n namespace:\n required: true\n description: Target namespace\n example: demo\n name:\n required: true\n description: IP Address Allocators name (lowercase alphanumeric and hyphens)\n example: example-address-allocator\n address_allocator_mode:\n required: true\n description: Address Allocator Mode\n allocation_unit:\n required: false\n description: Allocation Unit\n default: 0\n address_pool:\n required: false\n description: Address Pool\n default: value\n address_allocation_scheme:\n required: false\n description: "Server-required: Field should be not nil"\n default: value\nsteps:\n - id: navigate-to-list\n action: navigate\n url: /web/workspaces/multi-cloud-network-connect/manage/networking/legacy_network_configuration/address_allocators\n wait_for: text(\'IP Address Allocators\')\n description: Navigate to IP Address Allocators list page\n - id: click-add-tab\n action: click\n selector: text(\'Add IP Address Allocator\')\n wait_for: textbox[name=\'Name\']\n description: Click Add IP Address Allocator to open the create form\n - id: fill-name\n action: fill\n selector: textbox[name=\'Name\']\n value: "{name}"\n description: Enter Name\n - id: select-address_allocator_mode\n action: select\n selector: listbox\n context: Address Allocator Mode section\n value: "{address_allocator_mode}"\n description: Select Address Allocator Mode\n - id: fill-allocation_unit\n action: fill\n selector: spinbutton[name=\'Allocation Unit\']\n value: "{allocation_unit}"\n description: Set Allocation Unit\n - id: fill-address_pool\n action: fill\n selector: ngx-datatable input.form-control\n context: Address Pool table\n value: "{address_pool}"\n description: Enter Address Pool in the existing table row (no Add Item needed — the table ships one empty row)\n - id: select-address_allocation_scheme\n action: select\n selector: listbox\n context: Address Allocation Scheme section\n value: "{address_allocation_scheme}"\n description: Select Address Allocation Scheme\n - id: save\n action: click\n selector: "[class*=\'save-bt\'],[class*=\'submit-button\']"\n context: footer\n wait_for: text(\'{name}\')\n wait_timeout_ms: 30000\n description: Save/submit the form (union selector matches save-bt OR submit-button)\npostconditions:\n - resource_list_page_visible\n - "resource_name_in_list: {name}"\nmetadata:\n confidence: inferred\n discovered_at: 2026-06-24\n console_version: "2025.06"\n notes: Auto-generated by scripts/generate-workflows.ts from api-specs-enriched field metadata.\n',
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver for xcsh://fleet — "what kind of repository am I in, and what may I do here?"
|
|
3
|
+
*
|
|
4
|
+
* xcsh belongs to a fleet of repositories that are not interchangeable. Some hold
|
|
5
|
+
* demo and product content — documentation, Terraform plans, howtos — which xcsh
|
|
6
|
+
* authors directly. Some hold compiled code with their own build and test harness,
|
|
7
|
+
* where implementation belongs in a development environment and xcsh's deliverable
|
|
8
|
+
* is a verified issue. Some hold the fleet plumbing itself.
|
|
9
|
+
*
|
|
10
|
+
* That distinction is declared, not guessed: docs-control publishes `repo_classes`
|
|
11
|
+
* in `.claude/governance.json`, a managed file synced byte-identically into every
|
|
12
|
+
* governed repository. So the classification is readable offline from any checkout,
|
|
13
|
+
* and this resolver reads it from the working directory before reaching for the
|
|
14
|
+
* network.
|
|
15
|
+
*
|
|
16
|
+
* URL forms (host = "fleet"):
|
|
17
|
+
* - xcsh://fleet -> the current repository's class first, then the whole fleet
|
|
18
|
+
*
|
|
19
|
+
* Dynamic and uncached, mirroring xcsh://about. All shelling and file reading is
|
|
20
|
+
* injected so the resolver is unit-testable without a git repo, a `gh` binary, or
|
|
21
|
+
* a network.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { $ } from "bun";
|
|
26
|
+
import type { InternalResource, InternalUrl } from "./types";
|
|
27
|
+
|
|
28
|
+
export interface GhResult {
|
|
29
|
+
readonly ok: boolean;
|
|
30
|
+
readonly stdout: string;
|
|
31
|
+
readonly stderr: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FleetDeps {
|
|
35
|
+
readonly cwd: () => string;
|
|
36
|
+
/** Absolute path to the enclosing git repository root, or null when outside one. */
|
|
37
|
+
readonly repoRoot: (cwd: string) => Promise<string | null>;
|
|
38
|
+
/** The `origin` remote URL, or null when there is none. */
|
|
39
|
+
readonly repoOrigin: (cwd: string) => Promise<string | null>;
|
|
40
|
+
/** Contents of `<repoRoot>/.claude/governance.json`, or null when absent/unreadable. */
|
|
41
|
+
readonly readGovernance: (repoRoot: string) => Promise<string | null>;
|
|
42
|
+
readonly runGh: (args: string[]) => Promise<GhResult>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The repository the classification manifest is published from. */
|
|
46
|
+
export const GOVERNANCE_REPO = "f5-sales-demo/docs-control";
|
|
47
|
+
export const GOVERNANCE_RELPATH = ".claude/governance.json";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The organization's pre-rename name. Reads still redirect from it, but pushes are
|
|
51
|
+
* rejected, so a clone left on the old slug looks healthy until the first push
|
|
52
|
+
* fails. Classification keys are bare repository names, so the org does not affect
|
|
53
|
+
* the lookup — but it is worth warning about where we notice it.
|
|
54
|
+
*/
|
|
55
|
+
export const LEGACY_ORG = "f5xc-salesdemos";
|
|
56
|
+
export const CURRENT_ORG = "f5-sales-demo";
|
|
57
|
+
|
|
58
|
+
/** Authority values the manifest may declare for a class. */
|
|
59
|
+
export const AUTHORITY_AUTHOR = "author";
|
|
60
|
+
export const AUTHORITY_DELEGATE = "delegate";
|
|
61
|
+
export const AUTHORITY_GOVERNED = "governed";
|
|
62
|
+
|
|
63
|
+
/** Rendered class name when no manifest could be read at all. */
|
|
64
|
+
export const CLASS_UNCLASSIFIED = "UNCLASSIFIED";
|
|
65
|
+
|
|
66
|
+
export interface ClassDefinition {
|
|
67
|
+
readonly authority: string;
|
|
68
|
+
readonly description?: string;
|
|
69
|
+
readonly surfaces?: readonly string[];
|
|
70
|
+
readonly delegateTo?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RepoClasses {
|
|
74
|
+
/** `source_repo` from the governance manifest — the repository that published it. */
|
|
75
|
+
readonly sourceRepo: string;
|
|
76
|
+
readonly defaultClass: string;
|
|
77
|
+
readonly classes: Readonly<Record<string, ClassDefinition>>;
|
|
78
|
+
readonly repos: Readonly<Record<string, string>>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RepoVerdict {
|
|
82
|
+
readonly className: string;
|
|
83
|
+
/** True when the manifest names this repository explicitly. */
|
|
84
|
+
readonly declared: boolean;
|
|
85
|
+
/** False when the remote is outside this organization, so no class may be applied. */
|
|
86
|
+
readonly trustedOrg: boolean;
|
|
87
|
+
readonly definition: ClassDefinition | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A repository's identity as read from its `origin` remote. */
|
|
91
|
+
export interface RepoIdentity {
|
|
92
|
+
readonly org: string;
|
|
93
|
+
readonly name: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Organizations whose repository names the manifest is allowed to speak for. */
|
|
97
|
+
export const TRUSTED_ORGS: readonly string[] = [CURRENT_ORG, LEGACY_ORG];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read `repo_classes` out of a governance.json payload. Returns null — never throws —
|
|
101
|
+
* when the payload is malformed or predates the block, so a missing classification
|
|
102
|
+
* degrades into an actionable message rather than an error.
|
|
103
|
+
*/
|
|
104
|
+
export function parseRepoClasses(json: string): RepoClasses | null {
|
|
105
|
+
let parsed: unknown;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(json);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
112
|
+
const block = (parsed as Record<string, unknown>).repo_classes;
|
|
113
|
+
if (typeof block !== "object" || block === null) return null;
|
|
114
|
+
|
|
115
|
+
const raw = block as Record<string, unknown>;
|
|
116
|
+
const rawClasses = typeof raw.classes === "object" && raw.classes !== null ? raw.classes : {};
|
|
117
|
+
const rawRepos = typeof raw.repos === "object" && raw.repos !== null ? raw.repos : {};
|
|
118
|
+
|
|
119
|
+
const classes: Record<string, ClassDefinition> = {};
|
|
120
|
+
for (const [name, value] of Object.entries(rawClasses as Record<string, unknown>)) {
|
|
121
|
+
if (typeof value !== "object" || value === null) continue;
|
|
122
|
+
const def = value as Record<string, unknown>;
|
|
123
|
+
classes[name] = {
|
|
124
|
+
authority: String(def.authority ?? ""),
|
|
125
|
+
description: typeof def.description === "string" ? def.description : undefined,
|
|
126
|
+
surfaces: Array.isArray(def.surfaces) ? def.surfaces.map(String) : undefined,
|
|
127
|
+
delegateTo: typeof def.delegate_to === "string" ? def.delegate_to : undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const repos: Record<string, string> = {};
|
|
132
|
+
for (const [name, value] of Object.entries(rawRepos as Record<string, unknown>)) {
|
|
133
|
+
if (typeof value === "string") repos[name] = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A block with no classes is unusable; treat it as absent rather than render an
|
|
137
|
+
// empty fleet that looks authoritative.
|
|
138
|
+
if (Object.keys(classes).length === 0) return null;
|
|
139
|
+
|
|
140
|
+
const sourceRepo =
|
|
141
|
+
typeof (parsed as Record<string, unknown>).source_repo === "string"
|
|
142
|
+
? ((parsed as Record<string, unknown>).source_repo as string)
|
|
143
|
+
: "";
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
sourceRepo,
|
|
147
|
+
defaultClass: typeof raw._default === "string" ? raw._default : "",
|
|
148
|
+
classes,
|
|
149
|
+
repos,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Minimal shape of the session event bus, so this module does not depend on it. */
|
|
154
|
+
export interface CwdEventSource {
|
|
155
|
+
on(event: "cwd:changed", handler: (next: unknown) => void): unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A getter for the working directory the *session* is in, not the one the process
|
|
160
|
+
* started in.
|
|
161
|
+
*
|
|
162
|
+
* The bash tool keeps its own cwd and updates it on `cd`, emitting `cwd:changed`
|
|
163
|
+
* (`tools/bash.ts`); `process.chdir` is never called for it. Classifying against
|
|
164
|
+
* `process.cwd()` would therefore keep reporting the startup repository, so a
|
|
165
|
+
* `content` grant could survive after moving into a `developer` repository.
|
|
166
|
+
*/
|
|
167
|
+
export function createLiveCwdGetter(initialCwd: string, events?: CwdEventSource): () => string {
|
|
168
|
+
let current = initialCwd;
|
|
169
|
+
events?.on("cwd:changed", next => {
|
|
170
|
+
if (typeof next === "string" && next) current = next;
|
|
171
|
+
});
|
|
172
|
+
return () => current;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Split a GitHub remote URL into its org and bare repository name. */
|
|
176
|
+
export function repoNameFromOrigin(origin: string): { org: string; name: string } | null {
|
|
177
|
+
const cleaned = origin.trim().replace(/\.git$/, "");
|
|
178
|
+
const match = cleaned.match(/^(?:https?:\/\/|ssh:\/\/git@|git@)github\.com[:/]([^/]+)\/([^/]+)$/);
|
|
179
|
+
if (!match) return null;
|
|
180
|
+
const [, org, name] = match;
|
|
181
|
+
if (!org || !name) return null;
|
|
182
|
+
return { org, name };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a repository name to its class. An unlisted repository takes the
|
|
187
|
+
* manifest's `_default`, which docs-control pins to the most restrictive class, so
|
|
188
|
+
* forgetting an assignment withholds authority rather than granting it.
|
|
189
|
+
*/
|
|
190
|
+
export function classifyRepo(classes: RepoClasses | null, repo: RepoIdentity | null): RepoVerdict {
|
|
191
|
+
const trustedOrg = repo !== null && TRUSTED_ORGS.includes(repo.org);
|
|
192
|
+
if (!classes || !repo || !trustedOrg) {
|
|
193
|
+
// The manifest keys repositories by bare name, so an identically-named repository
|
|
194
|
+
// in another organization would otherwise inherit its class. Names are only
|
|
195
|
+
// meaningful inside the organization that published the manifest.
|
|
196
|
+
return { className: CLASS_UNCLASSIFIED, declared: false, trustedOrg, definition: null };
|
|
197
|
+
}
|
|
198
|
+
const declared = Object.hasOwn(classes.repos, repo.name);
|
|
199
|
+
const className = declared ? (classes.repos[repo.name] as string) : classes.defaultClass;
|
|
200
|
+
const definition = classes.classes[className] ?? null;
|
|
201
|
+
|
|
202
|
+
// Fail closed on our own side. Both the prompt and this document promise that an
|
|
203
|
+
// unnamed repository is treated as the restrictive case, and that promise must not
|
|
204
|
+
// depend on the publisher having set `_default` correctly: a typo or drift making the
|
|
205
|
+
// default an authoring class would otherwise hand out write authority to every
|
|
206
|
+
// repository nobody has classified yet. A named assignment is honoured as written;
|
|
207
|
+
// an unnamed one can never resolve to `author`.
|
|
208
|
+
if (!declared && definition?.authority === AUTHORITY_AUTHOR) {
|
|
209
|
+
return {
|
|
210
|
+
className: className || CLASS_UNCLASSIFIED,
|
|
211
|
+
declared,
|
|
212
|
+
trustedOrg,
|
|
213
|
+
definition: { ...definition, authority: AUTHORITY_DELEGATE },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { className: className || CLASS_UNCLASSIFIED, declared, trustedOrg, definition };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The behaviour each authority implies, stated so the agent does not have to infer it. */
|
|
221
|
+
function authorityGuidance(authority: string): string[] {
|
|
222
|
+
switch (authority) {
|
|
223
|
+
case AUTHORITY_AUTHOR:
|
|
224
|
+
return [
|
|
225
|
+
"**Authority: author.** Create, update and delete content here directly — documentation,",
|
|
226
|
+
"Terraform plans, howtos, diagrams, demo and traffic-generation scripts. You do not need to",
|
|
227
|
+
"ask permission to author; you do need to follow the governed path:",
|
|
228
|
+
"linked issue → branch → pull request → CI → auto-merge. Never commit to `main`.",
|
|
229
|
+
];
|
|
230
|
+
case AUTHORITY_DELEGATE:
|
|
231
|
+
return [
|
|
232
|
+
"**Authority: delegate.** This repository holds compiled or tested code with its own build",
|
|
233
|
+
"and test harness. Do not implement feature code here. File a CONTRIBUTING-compliant issue —",
|
|
234
|
+
"reproduce first, no unverified claims — and delegate the implementation to a development",
|
|
235
|
+
"environment (Claude Code / Codex). Reviewing, specifying and documenting are still yours.",
|
|
236
|
+
];
|
|
237
|
+
case AUTHORITY_GOVERNED:
|
|
238
|
+
return [
|
|
239
|
+
"**Authority: governed.** This is fleet plumbing: CI, packaging, container images, or",
|
|
240
|
+
"governance itself. Changes propagate to every repository, so they go through the governed",
|
|
241
|
+
"path only and are never made freehand.",
|
|
242
|
+
];
|
|
243
|
+
default:
|
|
244
|
+
return [
|
|
245
|
+
"**Authority: unknown.** The manifest does not declare an authority for this class.",
|
|
246
|
+
"Treat it as `delegate` — the restrictive case — and ask docs-control to fix the manifest.",
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderCurrentRepo(
|
|
252
|
+
slug: string | null,
|
|
253
|
+
identity: RepoIdentity | null,
|
|
254
|
+
verdict: RepoVerdict,
|
|
255
|
+
classes: RepoClasses | null,
|
|
256
|
+
legacyOrg: boolean,
|
|
257
|
+
): string[] {
|
|
258
|
+
const lines = ["## This repository", ""];
|
|
259
|
+
|
|
260
|
+
if (!slug) {
|
|
261
|
+
lines.push(
|
|
262
|
+
"Not inside a GitHub repository from this organization, so there is no class to apply.",
|
|
263
|
+
"Classify explicitly before authoring anything: read this document again from the repository",
|
|
264
|
+
"you intend to change.",
|
|
265
|
+
"",
|
|
266
|
+
);
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lines.push(`\`${slug}\` — class: **${verdict.className}**${verdict.declared ? " (declared)" : ""}`);
|
|
271
|
+
lines.push("");
|
|
272
|
+
|
|
273
|
+
if (!verdict.trustedOrg) {
|
|
274
|
+
lines.push(
|
|
275
|
+
`This remote is **outside \`${CURRENT_ORG}\`**, so the classification does not speak for it and`,
|
|
276
|
+
"**no authority is implied**. The manifest keys repositories by bare name, and a name only means",
|
|
277
|
+
"something inside the organization that published it — an identically-named repository elsewhere",
|
|
278
|
+
"is a different repository. Treat this as the restrictive case: do not author content here.",
|
|
279
|
+
"",
|
|
280
|
+
);
|
|
281
|
+
return lines;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (verdict.className === CLASS_UNCLASSIFIED) {
|
|
285
|
+
lines.push(
|
|
286
|
+
"No classification manifest was available, so **no authority is implied**. Do not author",
|
|
287
|
+
"content here on the strength of a guess.",
|
|
288
|
+
"",
|
|
289
|
+
);
|
|
290
|
+
return lines;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!verdict.declared) {
|
|
294
|
+
lines.push(
|
|
295
|
+
`This repository is **UNCLASSIFIED** — it is not named in the manifest, so it falls back to`,
|
|
296
|
+
`the fail-safe default (\`${classes?.defaultClass || "developer"}\`) and is treated as the most`,
|
|
297
|
+
"restrictive case. Ask docs-control to classify it rather than assuming authoring rights.",
|
|
298
|
+
"",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
lines.push(...authorityGuidance(verdict.definition?.authority ?? ""));
|
|
303
|
+
lines.push("");
|
|
304
|
+
|
|
305
|
+
if (verdict.definition?.surfaces?.length) {
|
|
306
|
+
lines.push(`Content surfaces: ${verdict.definition.surfaces.map(s => `\`${s}\``).join(", ")}`, "");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (legacyOrg) {
|
|
310
|
+
lines.push(
|
|
311
|
+
`> **This clone points at the pre-rename organization \`${LEGACY_ORG}\`.** Reads redirect, but`,
|
|
312
|
+
"> **pushes are rejected**, so the branch will look fine until you try to publish it. Fix it first:",
|
|
313
|
+
">",
|
|
314
|
+
"> ```",
|
|
315
|
+
`> git remote set-url origin https://github.com/${CURRENT_ORG}/${identity?.name ?? ""}.git`,
|
|
316
|
+
"> ```",
|
|
317
|
+
"",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return lines;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function renderFleet(classes: RepoClasses): string[] {
|
|
325
|
+
const byClass = new Map<string, string[]>();
|
|
326
|
+
for (const name of Object.keys(classes.classes)) byClass.set(name, []);
|
|
327
|
+
for (const [repo, className] of Object.entries(classes.repos)) {
|
|
328
|
+
if (!byClass.has(className)) byClass.set(className, []);
|
|
329
|
+
byClass.get(className)?.push(repo);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const lines = ["## Fleet", ""];
|
|
333
|
+
for (const [className, repos] of byClass) {
|
|
334
|
+
const def = classes.classes[className];
|
|
335
|
+
lines.push(`### ${className} (${repos.length}) — authority: ${def?.authority ?? "unknown"}`);
|
|
336
|
+
if (def?.description) lines.push("", def.description);
|
|
337
|
+
lines.push(
|
|
338
|
+
"",
|
|
339
|
+
repos.length > 0
|
|
340
|
+
? repos
|
|
341
|
+
.sort()
|
|
342
|
+
.map(r => `\`${r}\``)
|
|
343
|
+
.join(" ")
|
|
344
|
+
: "_none_",
|
|
345
|
+
"",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
lines.push(
|
|
350
|
+
"Any repository not listed above is **UNCLASSIFIED**. It is never granted authoring",
|
|
351
|
+
"authority no matter what the manifest's default says — an unnamed repository is always",
|
|
352
|
+
"treated as `delegate`, the restrictive case. Authority is never assumed from a",
|
|
353
|
+
"repository's contents; it is read from the manifest.",
|
|
354
|
+
"",
|
|
355
|
+
);
|
|
356
|
+
return lines;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Rendered when no manifest could be read — actionable, never a thrown error. */
|
|
360
|
+
function renderUnavailable(reason: string, slug: string | null): string {
|
|
361
|
+
return [
|
|
362
|
+
"# Fleet — classification unavailable",
|
|
363
|
+
"",
|
|
364
|
+
reason,
|
|
365
|
+
"",
|
|
366
|
+
"Until the classification is readable, **do not assume authority over any repository**: file an",
|
|
367
|
+
"issue and delegate rather than authoring content on a guess.",
|
|
368
|
+
"",
|
|
369
|
+
"Resolve it manually:",
|
|
370
|
+
"",
|
|
371
|
+
"```",
|
|
372
|
+
`gh api repos/${GOVERNANCE_REPO}/contents/${GOVERNANCE_RELPATH} --jq '.content' | base64 -d | jq .repo_classes`,
|
|
373
|
+
"```",
|
|
374
|
+
"",
|
|
375
|
+
`Or read \`${GOVERNANCE_RELPATH}\` in any governed checkout — the file is synced byte-identically`,
|
|
376
|
+
"across the fleet.",
|
|
377
|
+
"",
|
|
378
|
+
slug ? `Current repository: \`${slug}\`.` : "Not inside a GitHub repository from this organization.",
|
|
379
|
+
].join("\n");
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const FOOTER = [
|
|
383
|
+
"---",
|
|
384
|
+
"This classification decides *how* you contribute, not *whether* you do. In a `content`",
|
|
385
|
+
"repository, author directly through the governed path. In a `developer` repository, your",
|
|
386
|
+
"deliverable is a verified issue plus the specification, review and documentation around it —",
|
|
387
|
+
"the implementation belongs to a development environment. See `xcsh://source` for where xcsh's",
|
|
388
|
+
"own code lives and `xcsh://changes` for what shipped recently.",
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
/** Where this verdict's manifest came from, so the reader can judge how fresh it is. */
|
|
392
|
+
export type ManifestOrigin = "local" | "canonical";
|
|
393
|
+
|
|
394
|
+
function renderProvenance(origin: ManifestOrigin, classes: RepoClasses): string[] {
|
|
395
|
+
if (origin === "canonical") {
|
|
396
|
+
return [`_Classification read live from \`${GOVERNANCE_REPO}\`._`, ""];
|
|
397
|
+
}
|
|
398
|
+
return [
|
|
399
|
+
`_Classification read from this checkout's \`${GOVERNANCE_RELPATH}\`, published by`,
|
|
400
|
+
`\`${classes.sourceRepo}\`. It is synced fleet-wide, but a checkout that has not pulled`,
|
|
401
|
+
"recently can lag the canonical copy. If a verdict looks wrong, confirm it before acting:_",
|
|
402
|
+
"",
|
|
403
|
+
"```",
|
|
404
|
+
`gh api repos/${GOVERNANCE_REPO}/contents/${GOVERNANCE_RELPATH} --jq '.content' | base64 -d | jq .repo_classes.repos`,
|
|
405
|
+
"```",
|
|
406
|
+
"",
|
|
407
|
+
];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function renderFleetDoc(
|
|
411
|
+
slug: string | null,
|
|
412
|
+
identity: RepoIdentity | null,
|
|
413
|
+
verdict: RepoVerdict,
|
|
414
|
+
classes: RepoClasses,
|
|
415
|
+
legacyOrg: boolean,
|
|
416
|
+
origin: ManifestOrigin = "local",
|
|
417
|
+
): string {
|
|
418
|
+
return [
|
|
419
|
+
"# Fleet — repository classes and your authority here",
|
|
420
|
+
"",
|
|
421
|
+
...renderCurrentRepo(slug, identity, verdict, classes, legacyOrg),
|
|
422
|
+
...renderFleet(classes),
|
|
423
|
+
...renderProvenance(origin, classes),
|
|
424
|
+
...FOOTER,
|
|
425
|
+
].join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function defaultRunGh(args: string[]): Promise<GhResult> {
|
|
429
|
+
try {
|
|
430
|
+
const res = await $`gh ${args}`.quiet().nothrow();
|
|
431
|
+
return { ok: res.exitCode === 0, stdout: res.stdout.toString(), stderr: res.stderr.toString() };
|
|
432
|
+
} catch (err) {
|
|
433
|
+
return { ok: false, stdout: "", stderr: err instanceof Error ? err.message : String(err) };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export class FleetResolver {
|
|
438
|
+
readonly #deps: FleetDeps;
|
|
439
|
+
|
|
440
|
+
constructor(deps: FleetDeps) {
|
|
441
|
+
this.#deps = deps;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
445
|
+
const content = await this.#render();
|
|
446
|
+
return {
|
|
447
|
+
url: url.href,
|
|
448
|
+
content,
|
|
449
|
+
contentType: "text/markdown",
|
|
450
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
451
|
+
sourcePath: "xcsh://fleet",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async #render(): Promise<string> {
|
|
456
|
+
const cwd = this.#deps.cwd();
|
|
457
|
+
const root = await this.#deps.repoRoot(cwd);
|
|
458
|
+
|
|
459
|
+
let slug: string | null = null;
|
|
460
|
+
let identity: RepoIdentity | null = null;
|
|
461
|
+
let legacyOrg = false;
|
|
462
|
+
if (root) {
|
|
463
|
+
const origin = await this.#deps.repoOrigin(root);
|
|
464
|
+
const parsed = origin ? repoNameFromOrigin(origin) : null;
|
|
465
|
+
if (parsed) {
|
|
466
|
+
slug = `${parsed.org}/${parsed.name}`;
|
|
467
|
+
identity = parsed;
|
|
468
|
+
legacyOrg = parsed.org === LEGACY_ORG;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Prefer the local, offline copy: it is byte-identical to the published one
|
|
473
|
+
// across the whole governed fleet, so there is no reason to spend a network
|
|
474
|
+
// round trip when we are standing in a governed repository.
|
|
475
|
+
let classes: RepoClasses | null = null;
|
|
476
|
+
let origin: ManifestOrigin = "local";
|
|
477
|
+
let staleLocal = false;
|
|
478
|
+
let untrustedLocal = false;
|
|
479
|
+
if (root) {
|
|
480
|
+
const local = await this.#deps.readGovernance(root);
|
|
481
|
+
if (local !== null) {
|
|
482
|
+
const parsedLocal = parseRepoClasses(local);
|
|
483
|
+
if (parsedLocal && parsedLocal.sourceRepo !== GOVERNANCE_REPO) {
|
|
484
|
+
// A governance.json that names a different publisher is not the fleet's
|
|
485
|
+
// manifest — it may belong to an unrelated project or a fork. Ignore it
|
|
486
|
+
// and ask the canonical repository instead of trusting what is on disk.
|
|
487
|
+
untrustedLocal = true;
|
|
488
|
+
} else {
|
|
489
|
+
classes = parsedLocal;
|
|
490
|
+
staleLocal = parsedLocal === null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!classes) {
|
|
496
|
+
const remote = await this.#deps.runGh([
|
|
497
|
+
"api",
|
|
498
|
+
`repos/${GOVERNANCE_REPO}/contents/${GOVERNANCE_RELPATH}`,
|
|
499
|
+
"--jq",
|
|
500
|
+
".content",
|
|
501
|
+
]);
|
|
502
|
+
if (remote.ok && remote.stdout.trim()) {
|
|
503
|
+
const decoded = decodeMaybeBase64(remote.stdout.trim());
|
|
504
|
+
classes = parseRepoClasses(decoded);
|
|
505
|
+
if (classes) origin = "canonical";
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!classes) {
|
|
510
|
+
let reason: string;
|
|
511
|
+
if (untrustedLocal) {
|
|
512
|
+
reason =
|
|
513
|
+
`This checkout's \`${GOVERNANCE_RELPATH}\` is published by a different repository, not ` +
|
|
514
|
+
`\`${GOVERNANCE_REPO}\`, so it was not trusted — and querying \`${GOVERNANCE_REPO}\` did not succeed.`;
|
|
515
|
+
} else if (staleLocal) {
|
|
516
|
+
reason =
|
|
517
|
+
`This checkout's \`${GOVERNANCE_RELPATH}\` has no \`repo_classes\` block — the classification ` +
|
|
518
|
+
`is not yet published to this repository, and querying \`${GOVERNANCE_REPO}\` did not succeed either.`;
|
|
519
|
+
} else {
|
|
520
|
+
reason = `No \`${GOVERNANCE_RELPATH}\` was readable here, and querying \`${GOVERNANCE_REPO}\` did not succeed.`;
|
|
521
|
+
}
|
|
522
|
+
return renderUnavailable(reason, slug);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return renderFleetDoc(slug, identity, classifyRepo(classes, identity), classes, legacyOrg, origin);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* `gh api --jq .content` returns the base64 payload of a contents response. Accept
|
|
531
|
+
* either that or already-decoded JSON, so the caller does not have to care which
|
|
532
|
+
* form it got.
|
|
533
|
+
*/
|
|
534
|
+
function decodeMaybeBase64(raw: string): string {
|
|
535
|
+
if (raw.startsWith("{")) return raw;
|
|
536
|
+
try {
|
|
537
|
+
return Buffer.from(raw.replace(/\s+/g, ""), "base64").toString("utf-8");
|
|
538
|
+
} catch {
|
|
539
|
+
return raw;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function defaultRepoRoot(cwd: string): Promise<string | null> {
|
|
544
|
+
const git = await import("../utils/git");
|
|
545
|
+
return git.repo.root(cwd);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function defaultRepoOrigin(cwd: string): Promise<string | null> {
|
|
549
|
+
const git = await import("../utils/git");
|
|
550
|
+
return (await git.remote.url(cwd, "origin")) ?? null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function defaultReadGovernance(repoRoot: string): Promise<string | null> {
|
|
554
|
+
try {
|
|
555
|
+
const file = Bun.file(path.join(repoRoot, GOVERNANCE_RELPATH));
|
|
556
|
+
if (!(await file.exists())) return null;
|
|
557
|
+
return await file.text();
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function createFleetResolver(deps: Partial<FleetDeps> = {}): FleetResolver {
|
|
564
|
+
return new FleetResolver({
|
|
565
|
+
cwd: deps.cwd ?? (() => process.cwd()),
|
|
566
|
+
repoRoot: deps.repoRoot ?? defaultRepoRoot,
|
|
567
|
+
repoOrigin: deps.repoOrigin ?? defaultRepoOrigin,
|
|
568
|
+
readGovernance: deps.readGovernance ?? defaultReadGovernance,
|
|
569
|
+
runGh: deps.runGh ?? defaultRunGh,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
@@ -45,6 +45,7 @@ import { type ConsoleFieldMetadataData, EMPTY_CONSOLE_FIELD_METADATA } from "./c
|
|
|
45
45
|
import { type ConsoleResolver, createConsoleResolver } from "./console-resolve";
|
|
46
46
|
import { EMBEDDED_DOC_FILENAMES, EMBEDDED_DOCS } from "./docs-index.generated";
|
|
47
47
|
import extensionApiContent from "./extension-api.md" with { type: "text" };
|
|
48
|
+
import { createFleetResolver, type FleetDeps, type FleetResolver } from "./fleet-resolve";
|
|
48
49
|
import { createPluginResolver, type GetPluginRoots, type PluginResolver } from "./plugin-resolve";
|
|
49
50
|
import { createSourceResolver, type SourceResolver } from "./source-resolve";
|
|
50
51
|
import { createTerraformResolver, type TerraformResolver } from "./terraform-resolve";
|
|
@@ -66,6 +67,7 @@ const EXTENSION_HOST = "extension";
|
|
|
66
67
|
const PLUGIN_HOST = "plugin";
|
|
67
68
|
const CHANGES_HOST = "changes";
|
|
68
69
|
const SOURCE_HOST = "source";
|
|
70
|
+
const FLEET_HOST = "fleet";
|
|
69
71
|
const EMPTY_INDEX: ApiSpecIndex = { version: "unavailable", timestamp: "", domains: [] };
|
|
70
72
|
const EMPTY_CATALOG_INDEX: ApiCatalogIndex = {
|
|
71
73
|
version: "unavailable",
|
|
@@ -272,6 +274,8 @@ export interface InternalDocsProtocolOptions {
|
|
|
272
274
|
readonly apiSpecResolver?: ApiSpecResolver;
|
|
273
275
|
readonly apiCatalogResolver?: ApiCatalogResolver;
|
|
274
276
|
readonly getPluginRoots?: GetPluginRoots;
|
|
277
|
+
/** Injected so tests can classify without a git repo, a `gh` binary, or a network. */
|
|
278
|
+
readonly fleetDeps?: Partial<FleetDeps>;
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
@@ -285,6 +289,8 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
285
289
|
#pluginResolver: PluginResolver | null = null;
|
|
286
290
|
#changesResolver: ChangesResolver | null = null;
|
|
287
291
|
#sourceResolver: SourceResolver | null = null;
|
|
292
|
+
#fleetResolver: FleetResolver | null = null;
|
|
293
|
+
readonly #fleetDeps: Partial<FleetDeps> | undefined;
|
|
288
294
|
readonly #getPluginRoots: GetPluginRoots | undefined;
|
|
289
295
|
|
|
290
296
|
constructor(options: InternalDocsProtocolOptions = {}) {
|
|
@@ -294,6 +300,7 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
294
300
|
this.#apiCatalogResolver = options.apiCatalogResolver ?? null;
|
|
295
301
|
this.#terraformResolver = null;
|
|
296
302
|
this.#getPluginRoots = options.getPluginRoots;
|
|
303
|
+
this.#fleetDeps = options.fleetDeps;
|
|
297
304
|
}
|
|
298
305
|
|
|
299
306
|
#getApiSpecResolver(): ApiSpecResolver {
|
|
@@ -352,6 +359,13 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
352
359
|
return this.#changesResolver;
|
|
353
360
|
}
|
|
354
361
|
|
|
362
|
+
#getFleetResolver(): FleetResolver {
|
|
363
|
+
if (!this.#fleetResolver) {
|
|
364
|
+
this.#fleetResolver = createFleetResolver(this.#fleetDeps ?? {});
|
|
365
|
+
}
|
|
366
|
+
return this.#fleetResolver;
|
|
367
|
+
}
|
|
368
|
+
|
|
355
369
|
#getSourceResolver(): SourceResolver {
|
|
356
370
|
if (!this.#sourceResolver) {
|
|
357
371
|
this.#sourceResolver = createSourceResolver({ resolveBuildInfo: this.#resolveBuildInfo });
|
|
@@ -390,6 +404,10 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
390
404
|
return this.#getSourceResolver().resolve(url);
|
|
391
405
|
}
|
|
392
406
|
|
|
407
|
+
if (host === FLEET_HOST) {
|
|
408
|
+
return this.#getFleetResolver().resolve(url);
|
|
409
|
+
}
|
|
410
|
+
|
|
393
411
|
if (host === BRANDING_HOST) {
|
|
394
412
|
return this.#resolveBranding(url);
|
|
395
413
|
}
|
|
@@ -494,6 +512,7 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
494
512
|
const syntheticEntry = `- [${ABOUT_ROUTE}](${SCHEME_PREFIX}${ABOUT_ROUTE}) — identity and build fingerprint`;
|
|
495
513
|
const changesEntry = `- [${CHANGES_HOST}](${SCHEME_PREFIX}${CHANGES_HOST}) — recent merged PRs (what's new since your build), live via gh`;
|
|
496
514
|
const sourceEntry = `- [${SOURCE_HOST}](${SCHEME_PREFIX}${SOURCE_HOST}) — capability → source-path map and editable-surface rule`;
|
|
515
|
+
const fleetEntry = `- [${FLEET_HOST}](${SCHEME_PREFIX}${FLEET_HOST}) — this repository's class and what you may author here`;
|
|
497
516
|
const apiSpecEntry = `- [${API_SPEC_HOST}/](${SCHEME_PREFIX}${API_SPEC_HOST}/) — F5 XC API specifications (${specs.index.domains.length} domains, v${specs.version})`;
|
|
498
517
|
const apiCatalogEntry = `- [${API_CATALOG_HOST}/](${SCHEME_PREFIX}${API_CATALOG_HOST}/) — F5 XC API operation catalog (${catalog.summaries.length} categories, v${catalog.index.version})`;
|
|
499
518
|
const brandingEntry = `- [${BRANDING_HOST}](${SCHEME_PREFIX}${BRANDING_HOST}) — F5 XC branding and legacy name mapping (v${branding.version})`;
|
|
@@ -501,10 +520,11 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
501
520
|
const computerEntry = `- [${COMPUTER_ROUTE}](${SCHEME_PREFIX}${COMPUTER_ROUTE}) — machine hardware and environment profile`;
|
|
502
521
|
const tf = loadTerraformIndex();
|
|
503
522
|
const terraformEntry = `- [${TERRAFORM_HOST}/](${SCHEME_PREFIX}${TERRAFORM_HOST}/) — F5 XC Terraform provider (${Object.keys(tf.resources).length} resources, v${tf.version})`;
|
|
504
|
-
const
|
|
523
|
+
const entries = [
|
|
505
524
|
syntheticEntry,
|
|
506
525
|
changesEntry,
|
|
507
526
|
sourceEntry,
|
|
527
|
+
fleetEntry,
|
|
508
528
|
apiSpecEntry,
|
|
509
529
|
apiCatalogEntry,
|
|
510
530
|
brandingEntry,
|
|
@@ -512,9 +532,12 @@ export class InternalDocsProtocolHandler implements ProtocolHandler {
|
|
|
512
532
|
userEntry,
|
|
513
533
|
computerEntry,
|
|
514
534
|
...EMBEDDED_DOC_FILENAMES.map(f => `- [${f}](${SCHEME_PREFIX}${f})`),
|
|
515
|
-
]
|
|
516
|
-
|
|
517
|
-
|
|
535
|
+
];
|
|
536
|
+
// Derived, not hand-maintained: the advertised count used to be a magic
|
|
537
|
+
// offset that no test pinned, so adding a route silently produced a wrong
|
|
538
|
+
// header. Counting the entries we actually list cannot drift.
|
|
539
|
+
const listing = entries.join("\n");
|
|
540
|
+
const content = `# Documentation\n\n${entries.length} files available:\n\n${listing}\n`;
|
|
518
541
|
|
|
519
542
|
return {
|
|
520
543
|
url: url.href,
|
|
@@ -28,14 +28,26 @@ technical depth exists to serve the SE work.
|
|
|
28
28
|
Judgment: earned from production network incidents, security investigations, live
|
|
29
29
|
infrastructure deployments, and customer-facing technical engagements.
|
|
30
30
|
|
|
31
|
-
You
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
You are tuned as a **network-engineer assistant, not a coding assistant**. Your competence is
|
|
32
|
+
networking, security and cloud operations expressed through GitHub: comprehensive issues and PRs,
|
|
33
|
+
Terraform plans, JSON manifests, architecture and how-to documentation, diagrams, MEDDPICC and
|
|
34
|
+
account collateral, presentations, and authorized attack-traffic simulation scripts — and organizing
|
|
35
|
+
that netops/secops content into GitHub repositories. Your operational reach spans cloud-provider
|
|
36
|
+
CLIs, document and spreadsheet authoring, browser-driven F5 XC console automation, GitHub, and
|
|
37
|
+
CRM/pipeline data; the specific capabilities available in this session are the tools and installed
|
|
38
|
+
plugins listed below, not an assumed set.
|
|
39
|
+
|
|
40
|
+
Writing code is something you can do, but shipping feature code is not your job. Which repository
|
|
41
|
+
you are standing in decides how you contribute, and that is declared rather than guessed: read
|
|
42
|
+
`xcsh://fleet`. In a repository classified **content** you author directly — documentation,
|
|
43
|
+
Terraform, howtos, demo and traffic-generation scripts — through the governed path. In one
|
|
44
|
+
classified **developer** your deliverable is a rigorously-verified issue plus the specification,
|
|
45
|
+
review and documentation around it, and the implementation is delegated to a development
|
|
46
|
+
environment (Claude Code / Codex). In one classified **scaffolding**, changes go through the
|
|
47
|
+
governed path only. An unclassified repository is treated as **developer**.
|
|
48
|
+
|
|
49
|
+
You are also a **work in progress** under active development — you improve by verified
|
|
50
|
+
contribution, never by claiming. See `<self-awareness>`.
|
|
39
51
|
|
|
40
52
|
Document your reasoning: name the assumptions you're making, state the risks you see,
|
|
41
53
|
and confirm what you verified before yielding.
|
|
@@ -301,6 +313,7 @@ Most tools resolve custom protocol URLs to internal resources (not web URLs):
|
|
|
301
313
|
- `local://<TITLE>.md` — Finalized plan artifact created after `exit_plan_mode` approval
|
|
302
314
|
- `jobs://<job-id>` — Specific job status and result
|
|
303
315
|
- `mcp://<resource-uri>` — MCP resource from a connected server; matched against exact resource URIs first, then RFC 6570 URI templates advertised by connected servers
|
|
316
|
+
- `xcsh://fleet` — The class of the repository you are working in and what you may author there. **MUST** read before creating, updating, or deleting content in any repository of this organization — this is about the *current repository*, not about xcsh, so the gate on the other `xcsh://` documents does not apply.
|
|
304
317
|
- `xcsh://..` — Internal xcsh documentation. **MUST NOT** read unless the user asks about xcsh itself.
|
|
305
318
|
- `xcsh://about` — Identity, version, build fingerprint, architecture, self-improvement. **MUST** read for any question about xcsh before exploring `~/.xcsh/`.
|
|
306
319
|
This document contains the authoritative repository URL, issues URL, and source location.
|
|
@@ -331,6 +344,19 @@ X now / is Y fixed yet" are questions about xcsh itself — so you resolve them
|
|
|
331
344
|
- Recent changes / "what's new" → read `xcsh://changes` (live merged PRs; flags what shipped after your build).
|
|
332
345
|
- "Where is X implemented?" → read `xcsh://source`. Deeper identity / build / version → `xcsh://about`.
|
|
333
346
|
|
|
347
|
+
Which repository you are in also decides what you may do there, and it is **declared, not inferred
|
|
348
|
+
from the contents**. Before you create, update, or delete content in any repository of this
|
|
349
|
+
organization, read `xcsh://fleet` and act on its verdict:
|
|
350
|
+
- **content** → author directly. You do not need permission to write documentation, Terraform,
|
|
351
|
+
howtos, diagrams or demo scripts here; you do need the governed path — linked issue → branch →
|
|
352
|
+
pull request → CI → auto-merge, never a commit to `main`.
|
|
353
|
+
- **developer** → do not implement feature code. File a CONTRIBUTING-compliant issue (reproduce
|
|
354
|
+
first, no unverified claims) and delegate the implementation to a dedicated coding harness.
|
|
355
|
+
Specifying, reviewing and documenting remain yours.
|
|
356
|
+
- **scaffolding** → governed path only; these changes propagate fleet-wide.
|
|
357
|
+
- **unclassified** → treat as **developer**, the restrictive case, and say the repository needs
|
|
358
|
+
classifying. Never infer authoring rights from the fact that a repository contains documentation.
|
|
359
|
+
|
|
334
360
|
You are a **work in progress** under active development, improved through verified contribution:
|
|
335
361
|
- When you discover a newly-shipped feature, **offer to exercise** it (run the command or scenario it
|
|
336
362
|
touches) and report the evidence — do not claim it works from a PR title alone.
|
package/src/sdk.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
SkillProtocolHandler,
|
|
84
84
|
} from "./internal-urls";
|
|
85
85
|
import { buildComputerHint, loadComputerProfile } from "./internal-urls/computer-profile";
|
|
86
|
+
import { createLiveCwdGetter } from "./internal-urls/fleet-resolve";
|
|
86
87
|
import { loadProfile, type UserProfile } from "./internal-urls/user-profile";
|
|
87
88
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./ipy/executor";
|
|
88
89
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
|
|
@@ -1151,6 +1152,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1151
1152
|
}
|
|
1152
1153
|
},
|
|
1153
1154
|
getPluginRoots: () => listXcshPluginRoots(os.homedir(), cwd).then(r => r.roots),
|
|
1155
|
+
// Classification must follow the session's `cd`, not the process cwd, which
|
|
1156
|
+
// never moves. The bash tool emits `cwd:changed` when it relocates.
|
|
1157
|
+
fleetDeps: { cwd: createLiveCwdGetter(cwd, eventBus) },
|
|
1154
1158
|
}),
|
|
1155
1159
|
);
|
|
1156
1160
|
internalRouter.register(new JobsProtocolHandler({ getAsyncJobManager: () => asyncJobManager }));
|
|
@@ -15,7 +15,24 @@ export interface LlmsIndex {
|
|
|
15
15
|
fetchedAt: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Federated sites that are not F5 XC *product* documentation, and so do not belong
|
|
20
|
+
* in the product list this module feeds to the system prompt.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT the `repo_classes` authority classification from
|
|
23
|
+
* `xcsh://fleet`. The two answer different questions and disagree: `docs`,
|
|
24
|
+
* `cdn-simulator` and `origin-server` are all authority `content` — xcsh does author
|
|
25
|
+
* their documentation and Terraform — while being the documentation portal and two
|
|
26
|
+
* lab-infrastructure repositories rather than products. Driving this filter off
|
|
27
|
+
* authority would advertise all three as F5 XC products.
|
|
28
|
+
*
|
|
29
|
+
* The concern this tracks is the `llms-federated-sites.json` category in the `docs`
|
|
30
|
+
* repository: everything here is `build-platform`, `developer-tools`, `portal` or
|
|
31
|
+
* `lab-infrastructure`, and everything kept is `product-features` or
|
|
32
|
+
* `api-specifications`. That file is not synced into clones, so the list is
|
|
33
|
+
* maintained here.
|
|
34
|
+
*/
|
|
35
|
+
const NON_PRODUCT_SLUGS = new Set([
|
|
19
36
|
"docs-builder",
|
|
20
37
|
"docs-theme",
|
|
21
38
|
"docs-icons",
|
|
@@ -70,7 +87,7 @@ export function parseLlmsTxt(content: string, now?: Date): LlmsIndex {
|
|
|
70
87
|
|
|
71
88
|
const [, name, url, desc] = match;
|
|
72
89
|
const slug = extractSlug(url);
|
|
73
|
-
if (slug &&
|
|
90
|
+
if (slug && NON_PRODUCT_SLUGS.has(slug)) continue;
|
|
74
91
|
|
|
75
92
|
products.push({ name, description: desc, url });
|
|
76
93
|
}
|