diff --git a/docs/PLUGIN-PHILOSOPHY.md b/docs/PLUGIN-PHILOSOPHY.md index aea1bd4968..64d86a7146 100644 --- a/docs/PLUGIN-PHILOSOPHY.md +++ b/docs/PLUGIN-PHILOSOPHY.md @@ -32,21 +32,26 @@ statement does not settle it; `plugins/architecture/reference/topic-docs.md` is this rule entirely, being neither skill, agent, nor schema content — identifying the source is what the manifest is for.) -Like the setup contract below, **this is a normative target, not a description of the fleet**, and -enforcement reaches a strict subset of it. -`scripts/validate-plugin-contracts.mjs` gates the -marketplace id, `melodic-software/github-iac`, and `MELODIC_*` keys across every plugin's skill -content, and holds the `autonomy` plugin to a stricter token set; -`plugins/github/github.test.sh` sweeps a wider token set over a narrower scope — its own plugin's -prose only — as its "agnostic conformance" check, a sibling of that file's D4 zero-vendored-knowledge -checks, not one of them. The -bare organization name in skill prose is gated nowhere *fleet-wide* — only inside `autonomy` and -`github`, each by a sweep scoped to that one plugin — and agent content is gated nowhere at all, so -shipped skills predating this statement are nonconforming until brought into conformance rather than -absolved by a green build. A fleet-wide edit answers to two independent mechanisms, each running in -its own step of the same `plugin-gate` CI job and neither aware of the other; consolidating them -behind this statement, and -settling that conformance gap deliberately, is tracked in issue #3136. +Like the setup contract below, **this is a normative target, not a description of the fleet**. +Enforcement is the token classes in `scripts/org-agnosticism-tokens.txt` — one data file, every +site either reads it or is a documented narrowing/extension of it: + +- **fleet-id / fleet-key** — marketplace id, `melodic-software/github-iac`, and `MELODIC_*` keys, + across every plugin skill `.md` (`scripts/validate-plugin-contracts.mjs`). +- **setup** — setup-skill files must not bind to a marketplace name (same validator). +- **autonomy** — stricter extension: bare organization name and fleet repo names, scoped to the + `autonomy` plugin (`plugin.json` `author` remains exempt). +- **github** — this plugin's markdown only, adding `melodic`, `medley`, and `pulumi`. + `plugins/github/github.test.sh`'s "agnostic conformance" check is that extension, a sibling of + that file's D4 zero-vendored-knowledge sweeps, not one of them. The validator fails if that + test file is missing while the plugin exists, or if its regex drifts from the `github` class. + An unknown class name in the token file is a hard error. + +Agent content, schema files, and a fleet-wide bare organization name are **not gated**. That is a +deliberate narrowing of enforcement to the classes above, not an accident a green build absolves. +The independent `portability-lint` job stages a related publisher-token class in +`scripts/skill-portability-tokens.txt`; if that class activates it must consume or align with +`org-agnosticism-tokens.txt` rather than invent a third set. Keep plugins horizontally decoupled: diff --git a/scripts/org-agnosticism-tokens.txt b/scripts/org-agnosticism-tokens.txt new file mode 100644 index 0000000000..d01d9d9617 --- /dev/null +++ b/scripts/org-agnosticism-tokens.txt @@ -0,0 +1,41 @@ +# Org-agnosticism token set — the single data file every enforcement site +# reads or is a documented narrowing/extension of. +# +# Doctrine: docs/PLUGIN-PHILOSOPHY.md § "Design boundary". +# Idiom: same as scripts/skill-portability-tokens.txt (data, not gate logic). +# +# Format: blank lines and lines beginning with `#` are ignored. Every other +# line is ` ` (one space). Classes: +# +# fleet-id — marketplace id / publisher repo path, every plugin skill .md +# fleet-key — publisher-prefixed configuration keys, every plugin skill .md +# setup — setup-skill files must not bind to a marketplace name +# autonomy — autonomy plugin (stricter extension: bare org + fleet repos) +# github — github plugin prose (extension: this plugin's markdown). +# plugins/github/github.test.sh still carries the same regex +# as a plugin-local check; validate-plugin-contracts.mjs fails +# if that regex drifts from this class. +# +# Enforcement is deliberately these classes, not the full normative target +# (agent content, schema files, and a fleet-wide bare organization name stay +# ungated). Widening those is a separate decision; do not add a class here +# without remediating the corpus it would fail. An unknown class name is a +# hard error — a typo such as `fleet-keey` must not silently drop tokens. +# If `plugins/github` exists, `plugins/github/github.test.sh` is required +# and its agnostic-conformance regex must match this file's `github` class. +# +# The independent portability-lint job stages a related publisher-token class +# in scripts/skill-portability-tokens.txt. If that class activates, it must +# consume or align with this file rather than invent a third set. + +fleet-id @melodic-software\b +fleet-id melodic-software/github-iac +fleet-key \bMELODIC_[A-Z0-9_]+\b +setup @melodic-software\b +autonomy melodic-software +autonomy ci-workflows +autonomy github-iac +github melodic +github medley +github github-iac +github pulumi diff --git a/scripts/skill-portability-tokens.txt b/scripts/skill-portability-tokens.txt index 34af2d9125..672d1e6a69 100644 --- a/scripts/skill-portability-tokens.txt +++ b/scripts/skill-portability-tokens.txt @@ -136,6 +136,11 @@ origin/(main|master) # Residue at c7b0af78 — 1 hit / 1 file. Also near-green. # \*\*/\*\.cs([^a-zA-Z0-9_]|$) +# Publisher-org tokens (marketplace id, org name, fleet repo names, MELODIC_* +# keys) are NOT this file's job. They live in scripts/org-agnosticism-tokens.txt +# and are enforced by plugin-gate (#3136). If a publisher-token class is ever +# enabled here, consume or align with that file — do not invent a third set. +# # Forge/marketplace-internal hardcode: a hardcoded raw GitHub content URL where a # portable fetch or a declared scope belongs (#432). Enable when the forge-lock # members land (e.g. #441 declares source-control's inherent GitHub scope). diff --git a/scripts/validate-plugin-contracts.mjs b/scripts/validate-plugin-contracts.mjs index 66e70cab2b..d8416d07d0 100755 --- a/scripts/validate-plugin-contracts.mjs +++ b/scripts/validate-plugin-contracts.mjs @@ -7,6 +7,62 @@ import process from "node:process"; const root = process.cwd(); const failures = []; +// Org-agnosticism tokens live in scripts/org-agnosticism-tokens.txt — one +// data file, every site either reads it or is a documented extension (#3136). +// The class set is closed: a typo (`fleet-keey`) must fail, not drop tokens. +const ORG_AGNOSTICISM_CLASSES = Object.freeze([ + "fleet-id", + "fleet-key", + "setup", + "autonomy", + "github", +]); + +function loadOrgAgnosticismTokens() { + const byClass = Object.fromEntries(ORG_AGNOSTICISM_CLASSES.map((cls) => [cls, []])); + const path = join(root, "scripts", "org-agnosticism-tokens.txt"); + if (!existsSync(path)) { + failures.push(`scripts/org-agnosticism-tokens.txt: missing (org-agnosticism SSOT)`); + return byClass; + } + for (const raw of read(path).split(/\r?\n/)) { + if (!raw || raw.startsWith("#")) continue; + const match = raw.match(/^(\S+)\s+(\S+)\s*$/); + if (!match) { + failures.push(`scripts/org-agnosticism-tokens.txt: malformed line: ${raw}`); + continue; + } + const [, cls, ere] = match; + if (!ORG_AGNOSTICISM_CLASSES.includes(cls)) { + failures.push( + `scripts/org-agnosticism-tokens.txt: unknown class ${cls} (want ${ORG_AGNOSTICISM_CLASSES.join(", ")})`, + ); + continue; + } + byClass[cls].push(ere); + } + for (const cls of ORG_AGNOSTICISM_CLASSES) { + if (byClass[cls].length === 0) { + failures.push(`scripts/org-agnosticism-tokens.txt: no tokens for class ${cls}`); + } + } + return byClass; +} + +function orgAgnosticismRegex(pats) { + if (!pats || pats.length === 0) return null; + return new RegExp(pats.join("|"), "i"); +} + +const orgAgnosticismPats = loadOrgAgnosticismTokens(); +const orgTokens = { + fleetId: orgAgnosticismRegex(orgAgnosticismPats["fleet-id"]), + fleetKey: orgAgnosticismRegex(orgAgnosticismPats["fleet-key"]), + setup: orgAgnosticismRegex(orgAgnosticismPats.setup), + autonomy: orgAgnosticismRegex(orgAgnosticismPats.autonomy), + github: orgAgnosticismRegex(orgAgnosticismPats.github), +}; + function filesUnder(directory) { if (!existsSync(directory)) return []; const files = []; @@ -72,17 +128,17 @@ for (const path of setupContractFiles) { if (/pluginConfigs\s*\[\s*["'][^"']+@/i.test(content)) { fail(path, "must not write marketplace-qualified pluginConfigs keys"); } - if (/@melodic-software\b/i.test(content)) { + if (orgTokens.setup && orgTokens.setup.test(content)) { fail(path, "must not bind setup behavior to a marketplace name"); } } for (const path of pluginFiles.filter((path) => /[\\/]skills[\\/].*\.md$/.test(path))) { const content = read(path); - if (/@melodic-software\b|melodic-software\/github-iac/i.test(content)) { + if (orgTokens.fleetId && orgTokens.fleetId.test(content)) { fail(path, "reusable skill content must not require publisher-specific runtime identifiers"); } - if (/\bMELODIC_[A-Z0-9_]+\b/.test(content)) { + if (orgTokens.fleetKey && orgTokens.fleetKey.test(content)) { fail(path, "reusable skill content must not introduce publisher-prefixed configuration"); } } @@ -244,7 +300,7 @@ if (existsSync(aiBriefingBrandOverlay)) { // replace them; tool-specific detail lives in SKILL.md/README. const autonomyRoot = join(pluginRoot, "autonomy"); if (existsSync(autonomyRoot)) { - const fleetTokens = /melodic-software|ci-workflows|github-iac/i; + const fleetTokens = orgTokens.autonomy; const vendorTokens = /github|gitlab|bitbucket|slack|anthropic|claude|openai|copilot|cursor|devin/i; const autonomyReference = join(autonomyRoot, "reference") + sep; for (const path of filesIn(autonomyRoot)) { @@ -255,7 +311,7 @@ if (existsSync(autonomyRoot)) { delete manifest.author; content = JSON.stringify(manifest); } - if (fleetTokens.test(content)) { + if (fleetTokens && fleetTokens.test(content)) { fail(path, "autonomy plugin must not name the org or fleet repos (binding-seam owns instances)"); } if (path.startsWith(autonomyReference) && vendorTokens.test(content)) { @@ -319,6 +375,35 @@ if (existsSync(marketplacePath)) { } } +// github.test.sh's agnostic-conformance regex is a documented extension of +// this file's `github` class — same tokens, plugin-local reach. Drift here +// would recreate the two-set problem #3136 closed. If the plugin exists, the +// test file is required — a missing file must not skip the alignment check. +{ + const githubPlugin = join(pluginRoot, "github"); + const githubTest = join(pluginRoot, "github", "github.test.sh"); + if (existsSync(githubPlugin) && statSync(githubPlugin).isDirectory()) { + if (!existsSync(githubTest)) { + fail( + githubPlugin, + "github.test.sh is missing; keep the agnostic-conformance grep aligned with scripts/org-agnosticism-tokens.txt class github", + ); + } else if (orgTokens.github) { + const source = read(githubTest); + const found = source.match(/grep -riEn "([^"]+)" "\$PLUGIN_DIR" --include='\*\.md'/); + const expected = orgTokens.github.source; + if (!found) { + fail(githubTest, "agnostic-conformance grep not found; keep it aligned with scripts/org-agnosticism-tokens.txt class github"); + } else if (found[1] !== expected) { + fail( + githubTest, + `agnostic-conformance regex drifted from scripts/org-agnosticism-tokens.txt class github (file has ${found[1]}; tokens file has ${expected})`, + ); + } + } + } +} + if (failures.length > 0) { console.error("Plugin contract validation failed:"); for (const failure of failures) console.error(`- ${failure}`);