@clossys/launcher
v0.5.1
Published
Foundry workspace scaffolder: create, resume, or adopt a GitHub repository as the account hub. Executable tooling, not a role.
Maintainers
Readme
Foundry Launcher
Foundry Launcher (@clossys/launcher) is executable tooling
that makes one GitHub repository the account workspace hub. It is not a role
and does not claim adoption, grounding, or closure.
The hub inventories where Foundry packages are installed and coordinates engagement. It is not a product application and does not receive a dump of the catalogue.
Layout: the clossys/ folder
Every apply writes to one visible clossys/ folder in the target repository —
never a hidden dot-folder for anything a person might want to see. A generated
README.md at the root of clossys/ is an index of which clossys/<role>/ folders are active and what
each holds; it is rewritten on every run, never hand-edited. clossys/.state/
holds machine files only — the hub marker, the inventory, and the skills
manifest (below) — visible so it is easy to find, but still not a place to
edit by hand. Other roles' folders (clossys/strategist/, clossys/writer/,
and so on) are written by their own packages, not by launcher.
A hub created before this layout existed kept its marker and inventory under
a hidden .clossys/. Resume detects that automatically and migrates both
files to clossys/.state/, removing the old directory, and reports the move
in the health report. If somehow both a .clossys/ and a clossys/.state/
hub state exist at once, launcher refuses rather than guessing which one is
current — remove one and resume again.
Conversation contract
Every composed skill carries one shared conversation contract: lead with a plain-language status, always state a recommendation, ask exactly one question with the recommended option labelled first, and say what happens next. Launcher packs this contract at build time and injects it into each composed skill in place of that skill's own "how we work together" and "one question at a time" sections, at the same position — so an installed or catalogue skill's own wording never has to drift from it.
Skills manifest and freshness
Every apply writes a skills manifest recording each composed skill's source
(installed or catalogue), version, and a content digest. The health
report states how many composed skills are out of date against the live
@clossys/launcher version (catalogue-sourced skills are only ever as fresh
as the launcher release that packed them) and how many were retired this
run. Retirement means: a skill this directory's own manifest previously
listed is no longer composed (its source disappeared), so launcher removes
its composed output and host discovery links — and only that. It never
touches a skill it did not itself write.
The recorded digest is how launcher tells whether it still owns a composed
skill. Before rewriting or retiring one, it compares the file on disk
(.agents/skills/clossys-<package>/SKILL.md, and any real-directory copy
at a host discovery path) with the digest it recorded when it last wrote
that file:
| On disk | Rewrite (skill still composed) | Retire (skill no longer composed) | | --- | --- | --- | | Matches the recorded digest | Rewritten | Removed, with its discovery links | | Edited since launcher wrote it | Left as is and reported | Left as is, with its discovery links, and reported | | Missing | Recreated | Retirement completes (discovery links removed) | | No recorded digest (first run, or an older install) | Adopted if it already equals what launcher would write; otherwise left as is and reported | Not touched: launcher only retires a skill its manifest records |
A SKILL.md that exists but cannot be read (a permissions error, or a
directory in its place) is treated like an edited one: left as is and
reported. A retiring skill directory that holds files other than SKILL.md
is also left as is and reported; a macOS .DS_Store file is ignored for
this check. Each skill left as is appears in the health report as a
skill preserved line naming the file or directory that failed the check,
and in the report JSON under
skillComposition.preserved; it marks the report degraded, and it is
reported again on every run until resolved. Launcher recreates a missing
composed skill because .agents/skills is launcher-generated output, so
recreating it loses nothing a client wrote. That is also how to take
launcher's version of a skill you edited: move your copy aside, delete the
directory the skill preserved line names (usually
.agents/skills/clossys-<package>/), and run launcher again. To keep your
edit instead, leave the file as it is.
Health report and staleness
After create, resume, or appoint — and on every resume — the command prints
a read-only health report. It scans all four dependency buckets
(dependencies, devDependencies, optionalDependencies,
peerDependencies) for the two hub engine pins, @clossys/advisor and
@clossys/integrator (an advisor pin and an integrator pin line), and
for extra @clossys/* names.
When an engine's live registry version is known, each of its pins is graded
against it: a pin older than live is a stale pin finding, named by package,
and marks the report degraded. The report is also degraded when either
engine is missing, dual-pinned, or present in any bucket other than
devDependencies, when a composed skill in the hub was left as is because a
client edited it (the skill preserved lines above), when the hub's stored
inventory fails its contract, and when the hub's lockfile does not resolve the
engine pins yet (an install needed (engine-pins-changed-install-needed) line;
see "Engine pins and the lockfile" below). Per-package
skill sources missing from the catalogue are noted but do not by themselves mark
degraded. Each inventoried repository other than the hub gets a sibling line
(and an entry in skillComposition.siblings) saying what the run found for it:
a checkout beside the hub, one not cloned yet, another account's repository,
the Foundry supplier tree, a folder that is not a git checkout, a checkout git
refuses to read (dubious ownership), or a checkout whose git origin does not
match. The line names that repository by its position in the stored
inventory's repositories array (repositories[<i>] in the stored
inventory), never by its id (#1179), because the whole health report is also
JSON-dumped into the apply message's health: line. For a checkout beside the
hub or one not cloned yet, the line says a hub run writes nothing there, and
that once the repository is staffed in an approved plan, @clossys-advisor
and the voices of the roles staffed there arrive with that plan's setup pull
request. A sibling line never marks the report
degraded, and a sibling's working tree, output an earlier release wrote into it,
or its old pins do not change the hub run's result. Exit stays 0 on resume
(the report is advisory); adopt prints the same report and an unparseable
pin-versus-live comparison is noted as indeterminate rather than stale.
checkInventoryEntries() additionally validates hub inventory ids read-only,
marking ids whose repository no longer resolves (skipped with a note when
gh is unavailable).
Engine pins and the lockfile
For engine pins, resume and appoint change only the hub's package.json, never its lockfile,
and install nothing. A run that changes an engine pin says exactly what it
changed (engine pins changed in package.json: @clossys/advisor 0.2.6 -> 0.5.0;
@clossys/integrator added at 0.8.2, and health.enginePins in the JSON) and
gives one next step: run the hub's package manager install (npm install,
pnpm install, yarn install or bun install, by the lockfile present), then
commit package.json together with its lockfile. Until then a frozen install
(npm ci, pnpm install --frozen-lockfile, yarn install --immutable) refuses
the hub when a pin's version changed or an engine was added; when a pin only
moved between dependency buckets, or a range became the exact version already
locked, whether it refuses depends on the package manager (pnpm's frozen
install does), so such a change is reported the same way. While a lockfile is present that does not resolve the pins yet, the
report is degraded with an engine-pins-changed-install-needed finding
(health.installNeeded): an npm lockfile (npm-shrinkwrap.json, which npm
prefers when both exist, else package-lock.json) is read on every run, and
each engine pinned to a plain version must resolve to that same version in it,
compared as versions (a v0.6.0 pin matches a locked 0.6.0); any other lockfile is not read, so
it counts as unresolved for the engines the run changed. Resume and appoint
only raise a pin: one older than live, or not a plain version, becomes the live version,
and one newer than live is kept. Resume rewrites package.json as
2-space-indented JSON with a final LF, only when a pin changes, and never
changes its name.
Install
The get-started command is the package name:
npx @clossys/launchernpx caches the resolved version, so a plain npx @clossys/launcher can
keep running an old one. Because a catalogue-sourced skill is only ever as
fresh as the launcher release that packed it (see "Skills manifest and
freshness" above), run:
npx @clossys/launcher@latestwhen the health report says a skill is out of date, or whenever you want to be sure you are on the current release.
Public npm reads are credentialless. Packages publish to
https://registry.npmjs.org. Do not add a token or private registry
mapping for @clossys. Pin an exact version once you depend on the library
API:
npm install --save-dev --save-exact @clossys/[email protected]Talking to the team
First contact is npx @clossys/launcher (empty directory or the checkout you
appoint as the hub). After apply, the launcher composes the
@clossys-<package> voices into the hub (.agents/skills/clossys-<package>/
plus host discovery links). A launcher run writes no skills, skills manifest,
discovery links, AGENTS.md or CLAUDE.md into an inventoried product
repository, and changes nothing in its checkout beside the hub
(--clone-missing, below, only clones a missing one). Once a product
repository is staffed in an approved plan, it receives those files with that
plan's setup pull request; an inventoried repository that is not staffed does
not receive them. Composition is per checkout, not machine-wide, and is not a
catalogue dump into package.json.
Voices are how you talk in a coding agent; they are not engagement engines. The
@clossys/advisor npm package is the engine that grades evidence;
@clossys-advisor in chat is its hiring and compatibility voice. Use
@clossys-advisor and @clossys-<package> in the hub, where the whole team
is composed. A product repository staffed in an approved plan gets
@clossys-advisor and the voices of the roles staffed there, once that plan's
setup pull request has merged; a role not staffed there is expected to be
absent. Each voice can talk even when
that npm package is not pinned in that repo. When composing into the hub, the
launcher reads each skill body from the hub's installed
@clossys/<package>/skill/SKILL.md when present, then from the packed catalogue
or a sibling monorepo source. Launcher health
notes missing catalogue sources but apply continues.
Run npx @clossys/launcher again from the hub for a health report and to
refresh the voices composed in the hub. By default it does
not gh repo clone missing inventory entries -- that is not how you talk to
the team. launcher --clone-missing is the one explicit, approved
exception (#1179): on resume only, it clones every inventoried repository
not yet sitting beside the hub, using cloneMissingInventoryRepositories(),
and only those -- an id skipped for any other reason (wrong account, the
Foundry supplier tree, a mismatched git origin) is left exactly as skipped,
never attempted. Cloning is not composing: a repository cloned this way
receives its team only once it is staffed in an approved plan, like any other.
How to run it
There is no --org flag. Owner is inferred from authenticated gh and from
the current git remote. If more than one GitHub owner is visible, the
command asks which one should hold the hub. Non-interactive runs may set
CLOSSYS_OWNER.
GitHub-only. A GitLab, self-hosted, or other remote is a refusal, not a silent fallback.
| Current directory | What happens |
| --- | --- |
| Empty | Creates {owner}/workspace from the in-package skeleton (package name @owner/workspace, pinning live @clossys/advisor and @clossys/integrator exactly in devDependencies), or, when that repository already exists, clones it and classifies its hub marker like a local run: a current marker resumes; a legacy .clossys/ marker alone is migrated to clossys/.state/; both markers are refused before Launcher writes into the clone; with no marker, the clone is appointed as the hub (refused when its working tree has uncommitted changes; existing README.md, AGENTS.md and CLAUDE.md are kept, and a skeleton package.json is written only when the clone has none) and the apply message says "appointed". That appoint needs no inventory, but refuses when the clone carries an inventory that fails its contract or a package.json that is not a JSON object; every such refusal leaves the clone unchanged. |
| Already a hub (generated marker; packed template skeleton/clossys/.state/workspace.json) | Resumes. No new repository. --repositories writes the repositories the founder chose again into clossys/.state/inventory.json before skills are composed (see "Choosing the hub's repositories" below). --inventory here is refused, and the refusal points at choosing the repositories again on Advisor's repository card and running launcher --repositories, instead of at hand-editing the file. A legacy .clossys/ hub state is migrated automatically; see "Layout" above. For each hub engine whose live version the registry returned, pins that version exactly in devDependencies of the hub's existing package.json: a frozen older pin is bumped, a pin newer than live is kept, a missing Integrator pin is added, and a pin in another bucket is moved; other @clossys/* entries are left as they are, and the file is rewritten only when a pin changes. The report then names each change and the install to run next (see "Engine pins and the lockfile" above). |
| Any other GitHub repository you control | Appoints it as the account hub. Keeps existing product files. Refuses when the working tree has uncommitted changes (git status --porcelain non-empty) — the refusal names the offending remote host when the origin is not on github.com. Refuses when CLOSSYS_OWNER names a different account than the repository's github.com origin owner. Writes the hub marker. Pins live @clossys/advisor and @clossys/integrator, each exactly, in devDependencies, relocating any pin left in another bucket, raising an older one (a pin newer than live is kept), and leaving other @clossys/* entries as they are; the report names each change and the install to run next. An existing package.json name is kept; a {owner}/workspace checkout whose manifest has no name is named @owner/workspace. Refuses, before writing anything, an existing package.json that is not a JSON object (unparseable, an array, or a primitive). Always reads the public Advisor and Integrator versions (needed to pin live, and to pin and grade health on resume); refuses as indeterminate when the registry returns either one unreadable. Refuses if the generated hub inventory is missing or empty (packed template skeleton/clossys/.state/inventory.json; that generated path does not ship), and the refusal points the founder at choosing the hub's repositories on Advisor's repository card and passing them to --repositories, which writes the inventory (see "Choosing the hub's repositories" below). --inventory <path> still supplies a populated document instead — and, when the on-disk inventory is already populated and --inventory is also supplied, merges the two by repository identity (on-disk order first, new repositories appended, the first occurrence of a repository kept, and every kept entry kept whole, its packages included). Does not rewrite the lockfile or dump the catalogue. Prints a read-only health report. |
It does not have to be a brand-new exclusive repository, and it does not have to already match a Foundry layout. Informal "workspace-looking" trees are appointed by writing the hub contract, not assumed to be hubs already. Product applications may be appointed: they become the hub and remain the product. Sister repositories still pin their own roles. Appoint does not install packages.
Do not run this inside the Foundry supplier tree.
Open the resulting folder in your coding agent. Talk in ordinary sentences. Advisor stays read-only until you approve a next action. A yes in chat is permission for that one step only; it is not a lasting grant and it does not write git unless a file is saved later. The same command resumes later.
Choosing the hub's repositories
A founder never writes the inventory by hand (#1179). Advisor's repository
card (@clossys/advisor's repositoryChoiceCard(), or its
advisor-repository-card bin before the hub exists) offers the
repositories the agent listed from GitHub for the founder's sign-in; the
founder chooses; and the agent passes the chosen ids to Launcher:
launcher --repositories example-owner/example-app,example-owner/example-site--repositories takes one argument of comma-separated repository ids (an
id never contains a comma). It works when appointing a repository as the
hub and on an existing hub checkout; in an empty directory it is refused,
because there is no hub to write into yet. Launcher writes the chosen ids to
clossys/.state/inventory.json (a generated hub path, not shipped in this package)
-- the one inventory location it reads on
every later run -- and, on resume, writes it before the health report is
built, so the same run lists the repositories just chosen as sibling lines.
- The ids, and the document built from them, are checked against
docs/contracts/repository-inventory.json(in the public repository; that exact path does not ship in this package, but this package's build packs and ships its own copy of the contract) through the same shared contract checker every read of the inventory uses, and the document is checked again by that reader before it is written. What Launcher writes is what Launcher reads back. - A malformed choice -- an empty id, an id that is not a bare name or
owner/name, or two ids naming the same repository -- is refused by position (repositories[1].id ...), never echoed, and nothing is written. - Launcher decides "is this the same repository" one way everywhere: a
bare id names a repository of the hub's own owner, and letter case is
ignored, so
app,Appand<owner>/appare one repository. Owners are compared the same way. A stored inventory, or a choice, that lists one repository twice this way is refused, naming the two positions; it is never merged. The hub itself is recognised by its origin'sowner/name(or, without a github.com origin, the repository its marker records), never by its folder path, so an inventory that names the hub in another letter case never lists it as its own sibling. - An inventory that already lists exactly the chosen repositories, in any order or letter case, or with a bare id for the hub's own owner, is left as it is.
- An inventory that lists a different set is never merged into or
overwritten silently: the run is refused, stating how many repositories
each side has and which positions would be added and removed --
--repositories[<i>]for an added id's position in the--repositoriesargument,repositories[<i>] in the stored inventoryfor a removed id's position in the file -- never the ids themselves, because both the stored inventory file and--repositoriesare input an agent may relay verbatim, and a repository id is exactly the kind of short string a hostile inventory entry could shape as prompt-injection text. Run again with--replace-inventoryto approve the replacement; a repository that stays keeps its existing entry,packagesincluded. An agent acting on the refusal looks each reported position up in its own copy of the stored inventory file or its own--repositoriesargument to learn which repository it names, and tells the founder that name -- never the position string itself, and never text read back out of the inventory file or the argument without that lookup. That replacement run's own success line reports the same removed positions again, distinctly labeledrepositories[<i>] in the replaced inventory: by the time that line prints,clossys/.state/inventory.jsonis already the new file, so reusing "in the stored inventory" there would point a reader at the wrong document. The positions still index into the file as it stood before this run -- the same one the refusal step already named -- so an agent that already looked a position up there does not need to look it up again. The same position-only rule, and the same "in the stored inventory" wording, applies to every other message this command prints that names a repository from the file currently on disk -- eachsibling (...)line andlauncher --clone-missing's output. (Since #1511 theskill roster writtenhealth-report line names only the hub's own id, never a stored-inventory position, so it is no longer on this list.) - An inventory that fails its contract is likewise replaced only with
--replace-inventory. --repositoriesand--inventoryeach supply the whole inventory, so they are refused together, and--replace-inventorywithout--repositoriesis refused.
CLI
launcher
launcher --repositories example-owner/example-app,example-owner/example-site
launcher --repositories example-owner/example-app --replace-inventory
launcher --inventory path/to/inventory.json
launcher --clone-missing
launcher --help
launcher-check --help
launcher-check --input observation.json
launcher-doctor
launcher-apply-plan --plan plan.json --brief brief.json --repo ./product-checkout
launcher-apply-plan plan
launcher-apply-plan materialize --repo ./site-checkout
launcher-apply-plan verify --repo ./site-checkout
launcher-apply-plan status --repo ./site-checkout
launcher-apply-plan body --repo "<id>" --task-record 12
launcher-apply-plan snapshot --request package-request.jsonExit codes preserve the ternary:
| Exit | State | Meaning |
| --- | --- | --- |
| 0 | satisfied | Created, resumed, or appointed the hub. The message includes a read-only health report. |
| 1 | violated | Known refusal: not GitHub, not empty, missing appoint inventory, a malformed --repositories choice or one that differs from the stored inventory without --replace-inventory, the supplier tree, uncommitted changes in the appoint tree, or a CLOSSYS_OWNER that disagrees with the origin owner. |
| 2 | indeterminate | Missing gh, an Advisor or Integrator registry version that could not be read when creating or appointing, or an owner that could not be inferred. |
launcher-apply-plan has its own exit codes, described in Applying an approved plan and Taking the registry snapshot.
launcher-check grades a captured observation JSON through planWorkspace and does not create a hub. Same ternary: 0 is a create/resume/adopt plan, 1 is a known refusal, 2 could not run or could not decide. Appoint grades as a plan only when the observation already records a populated inventory; --inventory is a live CLI flag, not a check-cli input.
API
| Export | Description |
| --- | --- |
| planWorkspace() | Decides create, resume, or adopt from a cwd observation. Optional PlanWorkspaceOptions: { repositories, replaceInventory } (--repositories / --replace-inventory; appoint or resume) or { inventoryPath } (--inventory; appoint only) are the ways to appoint without a populated on-disk inventory. A plan carrying repositories holds the resulting ChosenInventory: the exact document to write, or unchanged. An appoint plan that merges --inventory into a populated stored inventory carries mergedInventoryRepositories, the merged entries, each kept whole, and mergedInventoryDocument, the exact merged document those entries render to, which apply writes. |
| applyWorkspacePlan() | Copies the in-package skeleton or hub marker through a host port and returns a WorkspaceApplyResult with health. Composes the skill voices (with the shared conversation contract injected) on the hub only, writes nothing into an inventoried checkout beside it, and reports each inventoried repository other than the hub under skillComposition.siblings; refreshes stale hub guidance and the generated clossys/ README on every path, including resume; migrates a legacy .clossys/ hub state automatically. Optional { skillCatalogueRoot, launcherPackageRoot, contractPath, liveLauncherVersion } selects where skill and contract bodies are read and grades skill-manifest staleness. |
| observeWorkspace() | Reads gh, git remotes, cwd, inventory classification, hub-state migration status, and the public Advisor and Integrator versions (npm view <package> version). |
| readInventoryRepositories() | Reads repository ids from an inventory file, as bytes, routed through validateInventoryDocument() (a missing file reads as no ids; anything present but invalid throws, naming the offending field by position -- never silently accepted or silently emptied). An optional fourth argument, the hub's owner, makes a bare id and <owner>/<id> one repository. |
| readLiveLauncherVersion() | Reads the public @clossys/launcher registry version, used only to grade catalogue-sourced skill staleness. |
| launcherPackageRootFromModule() | Resolves this package's root from import.meta.url so apply can find the packed skill catalogue and contract. |
| parseGitHubRemote() | Parses a github.com remote and rejects any other host. |
| isHubDocument() | Type guard for the generated hub marker (packed template: skeleton/clossys/.state/workspace.json). |
| inspectInventory() | Classifies an inventory document -- pass the file's bytes, and the hub's owner when known -- as missing, empty, populated, or invalid (malformed or schema-mismatched -- never silently folded into empty; see validateInventoryDocument()). |
| validateInventoryDocument() | Strictly validates an inventory document -- a file's exact bytes, or text built in memory -- against docs/contracts/repository-inventory.json (in the public repository; that exact path does not ship in this package, but this package's build packs and ships its own copy of the contract; schemaVersion: 1, a repositories array of { id, packages? } entries -- id a bare repository name or owner/name in the same format Launcher's own sibling/clone resolution requires, case-insensitively unique; packages, when present, shaped exactly as @clossys/integrator's InventoryPackageEntry, no other key). It is read as strict JSON (a syntax error is reported by position only; a key repeated in any object, bytes that are not valid UTF-8, a leading byte order mark, or a lone surrogate, escaped or raw, is refused) and checked by the same shared contract checker, packed from @clossys/advisor, that checks the plan and the brief. With InventoryReadOptions { hubOwner }, a bare id and <hubOwner>/<id> are also one repository, and a document listing both is refused. Returns { valid: true, ids } or { valid: false, reason }, where reason names every field at fault by position and never quotes a value. Every read and write of an inventory document -- --inventory, --repositories, the on-disk clossys/.state/inventory.json (a hub path, not shipped in this package) on every run, and readInventoryRepositories() -- passes the file's exact bytes, read with WorkspaceHost.readBytes(), never text decoded first, so invalid bytes cannot be silently replaced before the check; an inventory Launcher copies (--inventory, or a legacy .clossys/ inventory it migrates) is written back byte for byte; a document that merely resembles an inventory (for example a governance record whose entries also carry role, visibility, status, notes) is refused, never adopted or silently read as though it validated (#1334). |
| reportHubHealth() | Read-only pin, inventory, migration, and skills-manifest report: advisorPin and integratorPin, each engine graded against its own live version (the live Integrator version is the last, optional argument). Does not install or uninstall. |
| formatHubHealth() | Human lines plus a health: JSON line for the same report. |
| hasAdvisorPin() | True when a manifest already pins Advisor in any dependency bucket. |
| checkInventoryEntries() | Read-only inventory id validation through gh repo view (batched; skips with a note when gh is unavailable). |
| DEFAULT_REPOSITORY_NAME | Default hub repository name (workspace): the repository an empty-directory run creates, or clones when it already exists. |
| CLOSSYS_DIR_REL | Relative path of the one visible per-repository Clossys folder (clossys). |
| STATE_DIR_REL | Relative path of the machine-state folder (clossys/.state). |
| WORKSPACE_MARKER_REL | Relative path of the hub marker. |
| WORKSPACE_INVENTORY_REL | Relative path of the hub inventory. |
| CLOSSYS_README_REL | Relative path of the generated index README at the root of clossys/. |
| LEGACY_STATE_DIR_REL / LEGACY_WORKSPACE_MARKER_REL / LEGACY_WORKSPACE_INVENTORY_REL | Pre-#1171 .clossys/ paths, kept only so resume can detect and migrate them. |
| CommandResult / ChosenInventory / CwdObservation / DependencyBucket / EngineInstallFinding / EnginePinChange / HubDocument / HubEnginePin / HubHealthReport / HubMigrationState / InventoryObservation / InventoryValidationEntry / InventoryValidationReport / InventoryReadOptions / PinFinding / PinGrade / PlanWorkspaceOptions / SkillManifestDocument / SkillManifestEntry / SkillsManifestSummary / ApplyWorkspaceOptions / WorkspaceApplyResult / WorkspaceDecision / WorkspaceHost / WorkspaceObservation / WorkspacePlan / WorkspaceRefusal / WorkspaceState | Typed host, observation, plan, health, and outcome contracts. A WorkspaceHost reads and writes text and, for inventory files, exact bytes (readBytes() / writeBytes()). |
| cloneMissingInventoryRepositories() | Explicit, approved action (#1179): clones every inventoried repository of the hub's account that is not cloned beside the hub, and only those; a checkout already beside the hub is left out of the outcomes, and every other inventoried repository is reported as skipped-other-reason, never attempted. Returns a CloneMissingOutcome[]. |
| runDoctorChecks() | Read-only prerequisite checks in fix-in-this-order sequence: git, gh, signed in, Node.js, npm, then the advisory coding-agent step. Returns a DoctorReport. |
| renderDoctorReport() | Renders a DoctorReport one step at a time, the way launcher-doctor prints it. |
| checkCloudSessionBootstrap() | Read-only: the three product-repository-layout.json cloud-session-bootstrap checks against a directory. Returns a CloudBootstrapReport. |
| reportInventoryDrift() | Compares a declared external inventory against the launcher-written one; reports external-only, launcher-only, and agreeing repositories as a count plus each one's position, never its id (externalInventory[<i>] into the declared external document, repositories[<j>] into the hub's own stored inventory) -- both are document content, and this whole report is JSON-dumped into the apply message's health: line on every resume of a hub that declares externalInventory. Both files are read as bytes by the strict reader. An optional fifth argument, the hub's owner, compares ids as every other Launcher comparison does (a bare id is that owner's; case is ignored). The hub's own inventory is read with validateInventoryDocument(): a missing one lists nothing, and one that is present but invalid makes the report indeterminate, never a comparison against an empty list. Returns an InventoryDriftReport. |
| detectLinkedHosts() | Read-only: which of claude-code, cursor, codex can currently discover skills in a directory. |
| serializeHostRecord() / parseHostRecord() | Round-trip clossys/.state/hosts.json (HOSTS_REL). |
| parsePreferences() | Reads clossys/preferences.json's budget stance; defaults to "balanced" on absence or malformed input. |
| readHostModelProfile() | Reads a packed model-profiles/<host>.json; returns undefined, never throws, on a missing or malformed file. |
| resolveModelForTier() | Resolves a tier and budget preference to one model name for a host, reporting belowFloor rather than silently substituting a weaker tier's model. |
| validateAdvisorPlan() / validateEngagementBrief() | Validation of clossys/advisor/plan.json and clossys/brief.json (with its context snapshot) against the shared plan and brief contracts Advisor also validates against, including the contracts' code rules (R1-R11 for a plan, B1-B2 for a brief). Unknown fields are refused; the reason names every field at fault. |
| approvedSubject() | What an approval binds: the subjectDigest of the plan's latest decision (by timestamp) when that decision has chosen === "approved", else null. null when the approval has no subjectDigest, when decisions at the latest time disagree or name different subjects, when any decision time does not parse, or when the plan does not validate. Anything that applies a plan must use this; the one stated exception is the legacy brief-only path (applyEngagementBrief() and launcher-apply-plan), which predates the binding and uses isPlanApproved(). |
| isPlanApproved() | True only when a plan's most recent decision (by timestamp) has chosen === "approved". False when decisions at that latest time disagree, or when any decision time does not parse. It binds no bytes: it ignores subjectDigest, so it is also true for an approval that names no change. |
| applyEngagementBrief() | Writes clossys/brief.json into a repository directory once the plan validates and is approved and the brief validates; refuses and writes nothing otherwise. Reports the plan's canonical digest. |
| planDigest() / canonicalJson() / canonicalDigest() / PLAN_DIGEST_EXCLUDED_FIELDS | The canonical plan digest: sha256: over the RFC 8785 canonical JSON of the plan without asOf and decisions. Identical to Advisor's for every plan. canonicalDigest() is the shared step: sha256: over the canonical JSON of any value, which the plan, change-set and bundle digests all use. |
| planApplyBundle() | The pure apply planner: from a validated plan, the hub brief, observations of each staffed repository's default branch (including the exact bytes of its installed-state ledger and its composed-skill manifest), the change sets the hub holds, the composed skill text, the producer version and the hub's Advisor and Integrator pins, computes one change set per staffed repository (a setup set for a repository in the setup phase, an apply set otherwise) and returns a report-mode bundle. Trusts a repository's ledger only through held change sets, or skips the repository with the trust rule as its reason (ledger-unreadable, identity, renamed, ledger-chain, ledger-foreign-row); computes each owned path and key by compare-and-swap against the trusted ledger (add, keep, update, or a refusal: unowned-existing, client-edited, deleted), reported under V8; composes the Advisor voice with the staffed roles' voices; skips a repository, indeterminate and outside the bundle digest, when a setup set cannot be computed safely (package-manager-unsupported, starter-pin-absent, starter-pin-unsupported, release-age-text-absent, starter-request-invalid), when an apply set would change the Starter pin its request names (starter-request-stale), one whose Controller profile needs root entries added when the observation omits the profile text (root-entry-edit-unbuilt), one whose owned package the lockfile no longer resolves to the recorded version and integrity (integrity-mismatch, violated), one whose ledger holds only some of a setup template's files (template-rows-partial), and one whose lockfile or ledger path has a case variant among the observed files (case-variant-path). Two or more observed files at the same path, compared case-insensitively, have no single base digest between them, so that path is never kept, updated or adopted. Refuses, by throwing before computing anything, a staffed role that is not a lowercase id token (role-not-an-id) and a planItem that is not its repository id, a colon and its package name (plan-item-not-derived). Reads no file, network, process or clock; the same inputs give the same bytes. Throws, naming positions and never values, on inputs it cannot plan from. |
| AGENTS_GUIDE_PATH / AGENTS_GUIDE_TEXT / CLOSSYS_SKILL_PATTERNS / verifyAgentsGuide() | The Launcher-owned guide, the AGENTS.md file inside clossys/: its path, its one constant text, the three clossys-* skill path patterns the text carves out, and a check that is true only for exactly those bytes. The text holds no plan text, repository name or client detail, and no marker sits inside it. |
| trustInstalledLedger() / reconcileWholeFile() | Whether a repository's ledger bytes may be trusted: exactly canonical and valid (ledger-unreadable), for the observed node id (identity) and id, compared exactly -- a difference in letter case alone is still refused (renamed) -- every generation a held, valid change set whose digest recomputes and that agrees with its history entry (ledger-chain), and every row a write of the set it names (ledger-foreign-row); a refusal carries the rule only. reconcileWholeFile() is the compare-and-swap table for one whole file, with the generation-0 adoption pass allowed only in a setup set; clossys/.state/skills.json is adopted through that pass only when its base already holds the exact bytes the set would write, never merely because a skills manifest was read. Pure. Types: LedgerTrust, LedgerTrustRule, WholeFileState, WholeFileOutcome. |
| projectEngagementBrief() / serializeEngagementBrief() / PUBLIC_PROBLEM_PLACEHOLDER | One repository's brief: the hub brief with staffedHere set to that repository's roles in plan order, and problem replaced by the brief contract's fixed placeholder unless the repository is private, members in the brief contract's order at every depth; and the exact bytes written for it (two-space JSON and a final newline). |
| changeSetDigest() / changeSetDigestSubject() / CHANGE_SET_DIGEST_EXCLUDED_FIELDS / DERIVED_FILE_DIGEST_FIELDS | The change-set digest: canonicalDigest() of the change set without changeSetDigest, branch, bundle, pullRequest, inverse, tooling and texts, with each derived file reduced to path, mode, derived, item and invariants. |
| bundleDigest() | The bundle digest an approval binds: canonicalDigest() of the plan digest and, sorted by id, the id and change-set digest of each repository that has a change set. Nothing else. |
| validateRepositoryChangeSet() / validateApplyBundle() | Validation of a change set and a bundle against the shared change-set and bundle contracts, including their code rules (C1-C16 for a change set: references, allow-list and case-insensitive path rules, the ledger, the digest, only the ledger and lockfile derived, canonical order, each item's writes matching it -- discovery links, the skills manifest, the pointer files and the setup templates included -- a pin-starter in devDependencies and at most once, phase, a complete setup set, the release-age exemption's surface and scope, the root entries a Controller profile needs, no skill written through a symbolic link, every refusal at a path or key its item binds, every whole file changed only as its act's write kind allows (none deletes), and each planItem derived from the repository id and package name; A1-A7 for a bundle: unique ids, the digest, verdicts that are the worst of their checks, the authorization-mismatch and authorization-absent checks, no state or binding in a report bundle, and in a planned bundle a state only where all nine checks passed and a binding exactly where V3 passed). Unknown fields are refused; no reason echoes a value. |
| wouldViolateRootEntries() / isRootEntryName() | Whether the root names some paths introduce (each path's first segment) would fail a Controller repository profile's closed root vocabulary: satisfied when the profile has no vocabulary Controller checks (schema version 1 or 2, or an empty rootEntries) or declares every name allowed or required; violated, with the undeclared and the prohibited names, sorted; indeterminate (root-vocabulary-unknown) for anything Controller could not read as a root vocabulary. Reads only schemaVersion and rootEntries, by the rules Controller's README states; pure. isRootEntryName() is Controller's rule for one direct-child name. |
| validateInstalledLedger() / readInstalledLedger() / ledgerSuccession() / serializeInstalledLedger() / renderInstalledLedger() | The installed-state ledger (clossys/.state/installed.json): validation against the shared ledger contract and its code rules L1-L10 (history, each generation's approval binding, rows naming history, owned paths and link modes, keys matching packages, no act twice, canonical order, root entries in one Controller profile among fixed names, id-token roles in skill paths, and derived planItems); the contract's succession rules for a pull request's head ledger against its base's, each given as its exact bytes, a Uint8Array (both must be exactly canonical; then unchanged, or one next generation keeping the base's history, and an admitted generation installing exactly what the setup deferred and changing nothing else, except that an apply set may add the one guide row, for the AGENTS.md file inside clossys/, with the guide's digest), reporting whether a next generation was proved admitted or only claims an approval (approval-claimed); the exact bytes of a valid ledger; a strict read of a ledger from its bytes, also a Uint8Array (null unless it is a Uint8Array, valid and exactly canonical -- decoded with the same strict, BOM- and invalid-UTF-8-refusing reader the plan and brief contracts use, never a caller's own decode); and the bytes of the next generation a change set writes over the previous ledger, under the contract's RENDER section (each deferred row takes its identity from the plan's package acts, LedgerPackageIdentity), refusing a set computed from another generation or for another repository, and an apply set that would adopt a file. A valid ledger is well formed, not trusted: see trustInstalledLedger(). |
| storeChangeSet() / storeApplyBundle() / readStoredChangeSet() / readStoredApplyBundle() / CHANGE_SET_STORE_REL / BUNDLE_STORE_REL | The hub's content-addressed stores under clossys/.state/apply/change-sets/ and clossys/.state/apply/bundles/, one file per digest named by its 64 hex digits: a write validates first and does nothing when the file already holds the same bytes; a change set is append-only and different bytes under its name are refused, while a bundle's digest excludes its authorization, clock and verdicts, so storing a bundle under a digest that already names a file atomically replaces that one file with the newest computation (superseded computations are not recorded); a read is strict, validated, and returns the document only when its recomputed digest matches its name, else null. A digest argument that is not sha256: and 64 lowercase hex digits is refused before any path is built. A stored file's recomputed digest proves its integrity, not its provenance: anyone who can write the hub directory can add a set that verifies. Every store directory segment down to change-sets/ or bundles/ must be a real directory; a symbolic link anywhere in that chain is refused rather than followed, and a filesystem error other than a missing directory or file is rethrown naming only the operation and its error code, never a path. |
| CloneMissingOutcome / DoctorCheckHost / DoctorReport / DoctorStepId / DoctorStepResult / CloudBootstrapCheck / CloudBootstrapReport / ExternalInventoryDeclaration / InventoryDriftReport / DiscoveredHost / HostRecord / BudgetPreference / HostModelProfile / HostTierMapping / ModelResolution / PreferencesDocument / ReasoningTier / SupportedHost / AdvisorPlan / ApplyBriefResult / BlockerKind / EngagementBrief / EngagementBriefRole / EngagementContext / EngagementContextField / EngagementContextFieldId / GoalDirection / PlanBlocker / PlanDecision / PlanKit / PlanPackageAct / PlanStaffing / ValidationResult / PlanApplyBundleInputs / PlanApplyBundleResult / RepositoryObservation / SkippedRepositoryObservation / BundleDigestEntry / ApplyBundle / ApplyBundleRepository / ApplyCheck / ApplyCheckId / ChangeSetDeferral / ChangeSetItem / ChangeSetPhase / ChangeSetRefusal / CheckVerdict / ContentDigest / DependencyPlacement / DerivedFileChange / FileChange / KeyChange / LedgerInvariant / LockfileName / PackageInvariant / PackageManagerKind / PinnedPackage / RefusalReason / ReleaseAgeSurfaceKind / RepositoryChangeSet / RepositoryVisibility / WholeFileChange / ApprovalBinding / DiscoveryRoot / ExemptionSurfaceKind / WriteRecordSource / InstalledLedger / LedgerSuccession / LedgerViolation / RepositoryProfileObservation / RootEntryDeclaration / RootEntriesVerdict | Typed contracts for the sections above. |
Doctor
launcher-doctor (installed alongside @clossys/launcher) is read-only and
never writes anything (#1220). It checks the prerequisites a client needs
before the hub even exists -- git, the GitHub command-line tool, whether
you are signed in, Node.js, and npm -- and reports the first thing that is
missing, in plain language, with the one next action to take. Run it again
after fixing that one thing; it always reports the next thing, never a dump
of everything at once. A missing coding agent is reported but never blocks
the verdict: runDoctorChecks() marks it advisory, since Launcher cannot
detect every host and it is a one-time choice, not a step to fix in
sequence. renderDoctorReport() renders the report the way the CLI prints
it.
Product repositories
docs/contracts/product-repository-layout.json (this repository's own contract; it does not ship in the published package) extends the account hub's
clossys/ layout to a product repository: apps/*, workspace wiring so
@clossys/* packages install as exact pinned versions, AGENTS.md /
CLAUDE.md pointers, and the CI Starter proof (#1215). A cloud agent
session (browser plus GitHub, no local setup) can pick up a product
repository too -- checkCloudSessionBootstrap() verifies exactly what that
session needs: a resolvable package.json plus package-lock.json pair,
an AGENTS.md that mentions clossys/, and a hub marker at the same
relative path as the packed template skeleton/clossys/.state/workspace.json.
It never runs npm ci itself and never mutates anything; it only reports
which of the three is missing. A launcher run in the hub writes nothing
into a product repository, so until the repository is staffed in an approved
plan and that plan's setup pull request merges, an unsatisfied
agents-pointer check is the expected state; its note says so.
Setup templates
renderSetupTemplate() and the renderers beside it are pure functions that
return the exact bytes of the files a setup change writes. Nothing writes those
bytes yet; a later step plans them into a change set. The renderers behind it
are exported too: renderStarterRequest(), renderAdoptionDecisionWorkflow(),
renderProductCiWorkflow(), renderAdoptionEvidenceWorkflow(),
renderSnapshotCollector(), renderPathScopeWorkflow() and
renderPathScopeScript(), the standalone script the path-scope workflow embeds.
A TemplateResult is either { ok: true, files }, a list of TemplateFile
entries (path and bytes), or { ok: false, refusal } with a
TemplateRefusal; the package manager is a SetupPackageManager and the Starter
pin a StarterPinInput.
There are four acts, each with a fixed file list that renderSetupTemplate()
returns in this order:
add-caller-workflowtakes{ packageManager }and returns.github/workflows/clossys-adoption-evidence.yml,.github/workflows/clossys-adoption-decision.ymland.github/scripts/clossys-collect-adoption-snapshot.mjs.write-starter-requesttakes aStarterRequestInputand returns.starter/request.json.add-ci-templatetakes no input and returns.github/workflows/clossys-ci.yml.add-path-scope-jobtakes no input and returns.github/workflows/clossys-path-scope.yml.
The request is in the admission phase and names the Starter as both its own
engine and its target, and it names both evidence paths, the assessment file
and the target-input file. It carries no advisor and no hub. The Starter pin must be an exact version in >=0.2.0 <0.4.0
(STARTER_PIN_RANGE); any other pin is refused as starter-pin-unsupported,
and a package manager other than npm or pnpm, Yarn included, is refused as
package-manager-unsupported. A refusal names a position such as
starter.version and never quotes the value it refused.
The decision workflow starts only on workflow_run completion of the evidence
workflow, and its job carries no condition, so it starts for every conclusion.
It checks out the protected pull request base, runs one fixed frozen install
(npm ci --ignore-scripts or pnpm install --frozen-lockfile --ignore-scripts),
and runs the installed Starter's admit command over a sparse checkout of the
workflow_run head that holds only /clossys/.state/installed.json. The
trusted decision job never reads or trusts the snapshot artifact the evidence
workflow uploads, because a pull request controls that workflow. The collector
script is still written because the contract's file set for the caller
workflows names it.
The path-scope job applies to pull requests whose head branch starts with
clossys/apply-, and fails when a changed path is outside the paths Clossys may
own (OWNED_PATH_PATTERNS) or, apart from the ledger, package.json and the
lockfiles, is not named by the pull request's own ledger. It runs in the pull
request's own context, so it catches an agent's mistakes, not a hostile author;
the admission job runs from the protected base.
A change to any of these workflows, or to .starter/request.json, is proved
only by the first pull request after it merges, because the decision runs from
the base: a one-merge lag.
Inventory: adopting an existing source
When an account already keeps a repository inventory in its own control
plane, launcher reads it as the source of truth instead of writing a
second, diverging one (#1216). Declare it by hand-editing the hub marker (the packed template
skeleton/clossys/.state/workspace.json; the generated path does not
ship) to add an externalInventory: { path, shape } field (a "foundry"
or "custom" shape). Every launcher run
(create, resume, or appoint) then calls reportInventoryDrift()
automatically and prints the result in hub health output when the marker
declares one: three sets, all three even when one is empty -- ids only in
the external source, ids only in launcher's own inventory, and ids both
agree on. A "custom" shape is reported indeterminate rather than guessed
at -- launcher has no mapping for a non-foundry inventory shape yet. It
never merges the two silently; writing the reconciled set is left as a
separate, explicit apply step for a follow-up.
Hosts
Every launcher run (create, resume, or appoint) records which
coding-agent hosts a directory could already discover skills through,
before that run composes skills and stamps every host's discovery path
(#1180): claude-code and cursor are detected by their own discovery
symlink (.claude/skills, .cursor/skills); codex is detected by the
presence of .agents/skills itself, since Codex reads repository skills
from that path directly and needs no separate discovery link (verified
against developers.openai.com/codex/skills, 2026-09-22). The snapshot is
written to clossys/.state/hosts.json (HOSTS_REL, via
serializeHostRecord() / parseHostRecord()) for the hub, the one checkout
a launcher run composes skills into -- so a consumer such as
Advisor's next-action phrasing can name the client's actual tool instead
of guessing.
Model guidance
Packages declare what a step demands -- a reasoning tier (light /
standard / deep) -- never a model name (#1219). Launcher ships the
tier-to-model mapping per host in model-profiles/<host>.json, dated and
re-verified against that host's real current models;
readHostModelProfile() reads one. parsePreferences() reads
clossys/preferences.json's budget stance (cost-conscious / balanced /
max-quality, defaulting to balanced when absent or malformed) --
Advisor asks the question and writes the file; this package only resolves
against it. resolveModelForTier() combines a profile, a tier, and a
preference into one ModelResolution, and reports belowFloor rather than
silently substituting a weaker model when a caller-supplied hard floor
tier cannot be met.
Applying an approved plan
launcher-apply-plan --plan <plan.json> --brief <brief.json> --repo <dir>
writes clossys/brief.json into a staffed repository once the plan is
approved (#1178). validateAdvisorPlan() and validateEngagementBrief()
check both files against the shared contracts in this repository's
docs/contracts/ -- advisor-plan.json, engagement-brief.json, and
engagement-context.json for the brief's context snapshot (#1475). This
package's build packs those files, with a copy of the one contract checker
@clossys/advisor uses, so Launcher and Advisor accept exactly the same
plans and briefs while Launcher keeps no runtime dependency on Advisor.
Every object is closed: a field the contracts do not declare is refused,
and a known context value must be one of that field's fixed choice ids,
because the brief is committed in every staffed repository. A string or
key containing a lone surrogate is refused, so every plan that validates has
a digest. Every plan time must be a real calendar date or date-time in
ISO 8601 form, a date-time with Z or a ±hh:mm offset, checked
field by field (so 2026-02-30 or T24:30 is refused). A brief's
problem, role, why and metric must contain a non-whitespace
character, and an item of inputsFrom, outputsTo, sequence or
deliverables must not be empty. A refusal names each declared field at
fault and never echoes its value, and never names a key the contracts do
not declare: such a field is reported at the object that holds it, by its
1-based position there (plan.mandate has a field the contract does not
declare (key 4 of this object), and unknown fields are refused), counted
in the order the file wrote the keys when launcher-apply-plan reads it; a
value a caller passes to a validator directly is counted in JavaScript's
own key order, which lists array-index keys such as "7" first.
launcher-apply-plan reads both files as strict JSON: bytes that are not
valid UTF-8, a leading byte order mark, or an object that repeats a key at
any depth exit 2, with a repeated key reported by its position in its
object and, below the top level, that object's position, never by name,
and a syntax error by position only, never quoting the file's text, so the
value validated is exactly the one a reader of the file sees. A position is
a 0-based index into the decoded text in UTF-16 code units (JavaScript's
string index): the byte offset for ASCII text, with a character outside
the Basic Multilingual Plane counting as two.
A plan may say which roles work in which repository (staffing, by
repository inventory id), which kits were recommended (kits), which exact
package acts are authorized (packages, each one exact version and one
sha512- integrity value) and where those versions were resolved from
(resolution) (#1178). Once the schema passes, the contract's code rules
run: no repository is staffed twice (ids compare case-insensitively),
every staffed role is one of the mandate's roles and every mandate role is
staffed somewhere unless it is a hub-only role, every
package act names a staffed repository spelled exactly the same, no
planItem repeats, no package appears twice in one repository,
resolution is present exactly when packages is, no kit id repeats, no
role repeats within one staffing entry, no role is named twice in
mandate.roles, a repository has at most one pin-starter act, always
placed in devDependencies, no hub-only role is staffed, and every
planItem is exactly its act's repository, a colon and its name, in the
same letter case (R12). The hub-only
roles, Advisor and Integrator, are read from the plan contract's
definitions.hubOnlyRoles, the same data Advisor reads. The rules read only a document's own fields,
as the schema does, so an inherited one is ignored. A brief's optional staffedHere
must name only the brief's own roles, each once. This package implements
those rules separately from Advisor, and both are tested against the same
corpus, docs/contracts/advisor-plan-rules.fixture.json (in the public
repository, not shipped in this package).
approvedSubject() says what an approval binds: the subjectDigest of the
plan's most recent decision (by timestamp, not array position) when it is
"approved". It validates the plan itself first and returns null for one
that does not validate. An approval with no subjectDigest binds nothing,
and so does no decision at all, a decision time that does not parse, or a tie at
the latest instant between decisions that disagree or name different
subjects. It says what was approved, not that it matches: a caller must
recompute the digest of the change it holds and compare.
isPlanApproved() reads the same most recent decision and is true when it
is "approved", with the same rules for ties and unreadable times, but it
binds no bytes: it ignores subjectDigest, so it is also true for an
approval that names no change. Anything that applies a plan must use
approvedSubject() instead.
applyEngagementBrief() and launcher-apply-plan are the brief-only path,
which predates that binding and is kept as it was, not extended: they
require isPlanApproved() and accept an approval with or without a
subjectDigest, checking no binding.
applyEngagementBrief() refuses, and writes nothing, unless all three
checks pass and the plan's digest is computed; only then does it write the brief byte-identically -- it never re-authors
its prose -- and reports planDigest() of the plan it applied, which the
CLI prints as plan digest sha256:.... That digest is defined once, in
docs/contracts/advisor-plan-digest.md (in the public repository, not shipped in this package);
this package and Advisor each
implement it and are tested against the same fixture corpus. On this
brief-only path, this package does not compute the brief's content -- it
writes the brief it is given, which @clossys/advisor's
toEngagementBrief() builds, and applies no per-repository projection --
and it does not decide whether a plan should be approved (that is
Advisor's job); it only validates the two shapes and writes the one file.
The apply planner below is different: it projects each repository's brief
from the hub brief itself.
Launcher's packed skill carries the agent procedure that puts a stored change
set into a staffed repository, under "Apply an approved plan" (#1762). The
agent verifies, files the task-record issue, prints the pull request body with
launcher-apply-plan body, commits and pushes only the set's clossys/apply-
branch, opens the pull request, reads launcher-apply-plan status, and reports.
Launcher never pushes, opens a pull request, files an issue or merges, and the
procedure forbids merging and enabling auto-merge. check-package-skills pins
each of those rules in the packed skill text.
Release-age exemption
editReleaseAgeExemption() computes the edit that lists the publishing
scope's <scope>/* entry as exempt from a package manager's release-age
delay (#1178). It is pure: it does no I/O. It takes a surface
(pnpm-workspace or yarnrc), the surface file's text or null, and, for
pnpm, the .npmrc text or null. It returns edited with the exact new
text, unchanged when the scope entry is already listed, or a refusal
(ReleaseAgeEdit, with ReleaseAgeEditInput and
ReleaseAgeEditRefusalReason).
The entry goes under minimumReleaseAgeExclude (pnpm, single-quoted) or
npmPreapprovedPackages (Yarn, double-quoted). A missing file becomes the
key alone, a top-level block sequence of scalars gets one new entry after
its last item, and a file without the key gets the key appended. Other
bytes, including comments and the final newline, are kept. The function
reads only the shapes it recognises: release-age-surface-unparseable covers
a flow sequence, an anchor, an alias, a tag, a comment inside the list,
several documents, a tab, a carriage return or byte order mark, a repeated
key, and a value that is not a block sequence of scalars.
For pnpm the .npmrc is read under a fixed grammar and refused otherwise.
Every line must be blank, a comment (first non-space character # or ;),
or a plain key=value assignment, optionally spaced around the =, whose
base key is only ASCII letters, digits and @ : _ . / -. An unrelated base
key may have one immediate empty [] suffix. An .npmrc containing any
line outside those shapes (a quoted key, an indexed, repeated, spaced or
malformed bracket suffix, a comment or escape inside the key, a tab, a
section header, a key with no =) is refused as
release-age-surface-unparseable, because npm's ini reader could read such a
line as the exclusion setting. A base key that is userconfig,
globalconfig or prefix (in any case, with - and _ ignored), including
its array form, is refused the same way, because it points npm at another
config file or prefix directory whose own exclusion list the editor cannot
read. A base key that is minimum-release-age-exclude in any case, with -
and _ ignored (so the camel-case spelling too), including its array form,
is refused as release-age-surface-conflict. Refusing is the default: an
unrelated .npmrc line the grammar does not list also refuses the whole
file, and the caller resolves the file by hand.
verifyReleaseAgeExemption() takes the surface, the text before, the text
after, and, for pnpm, the .npmrc text. It reports a ReleaseAgeVerdict, { verified: true, value }
(its input is a ReleaseAgeVerifyInput), only when the two texts differ by that one added entry, read again with the
same rules; value is the <scope>/* string the installed-state ledger's
entries row holds. It returns { verified: false } for an unchanged file
(before equal to after) and for any .npmrc that is a conflict or outside
the grammar above, so a wiring unit must not verify after an unchanged or
refused result. It says nothing about whether a given pnpm or Yarn
version honours the key; that is proved separately with pinned tools.
Computing each repository's change
planApplyBundle() computes, for each repository a plan staffs, the change
set one pull request would make there, and a bundle that holds them (#1178).
It is pure: it takes the plan, the hub brief (clossys/advisor/brief.json),
what the caller observed on each repository's default branch (including the
exact bytes of its installed-state ledger and its composed-skill manifest),
the change sets the hub holds, the composed skill text for each role and for
Advisor, this package's version and the hub's Advisor and Integrator pins,
and it reads nothing itself. The shapes are the shared contracts
docs/contracts/repository-change-set.json and apply-bundle.json, packed
into this package, and every set and the bundle are validated against them,
code rules included, before they are returned. The digests are defined in
docs/contracts/apply-change-set-digest.md, with a corpus computed
independently of this package (both in the public repository, not shipped
in this package).
- Each set describes the repository's brief, projected from the hub brief with
staffedHereset to its roles and, unless the repository is private, the br
