@clossys/launcher 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +51 -2
  2. package/dist/admission-fixture.d.ts.map +1 -1
  3. package/dist/admission-fixture.js +15 -1
  4. package/dist/admission-fixture.js.map +1 -1
  5. package/dist/admission.d.ts.map +1 -1
  6. package/dist/admission.js +6 -1
  7. package/dist/admission.js.map +1 -1
  8. package/dist/apply-plan-cli.d.ts +1 -1
  9. package/dist/apply-plan-cli.d.ts.map +1 -1
  10. package/dist/apply-plan-cli.js +1 -1
  11. package/dist/approval-sheet.d.ts.map +1 -1
  12. package/dist/approval-sheet.js +6 -0
  13. package/dist/approval-sheet.js.map +1 -1
  14. package/dist/change-set-contract.d.ts +22 -0
  15. package/dist/change-set-contract.d.ts.map +1 -1
  16. package/dist/change-set-contract.js +45 -2
  17. package/dist/change-set-contract.js.map +1 -1
  18. package/dist/existing-declaration-adoption.check.d.ts +2 -0
  19. package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
  20. package/dist/existing-declaration-adoption.check.js +10 -0
  21. package/dist/existing-declaration-adoption.check.js.map +1 -0
  22. package/dist/generated/plan-contracts.generated.d.ts.map +1 -1
  23. package/dist/generated/plan-contracts.generated.js +264 -3
  24. package/dist/generated/plan-contracts.generated.js.map +1 -1
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/ledger-contract.d.ts +5 -2
  30. package/dist/ledger-contract.d.ts.map +1 -1
  31. package/dist/ledger-contract.js +25 -2
  32. package/dist/ledger-contract.js.map +1 -1
  33. package/dist/ledger-trust.d.ts.map +1 -1
  34. package/dist/ledger-trust.js +8 -3
  35. package/dist/ledger-trust.js.map +1 -1
  36. package/dist/materialize.d.ts.map +1 -1
  37. package/dist/materialize.js +43 -2
  38. package/dist/materialize.js.map +1 -1
  39. package/dist/plan-bundle.d.ts +7 -1
  40. package/dist/plan-bundle.d.ts.map +1 -1
  41. package/dist/plan-bundle.js +64 -9
  42. package/dist/plan-bundle.js.map +1 -1
  43. package/dist/plan-command.d.ts +1 -1
  44. package/dist/plan-command.d.ts.map +1 -1
  45. package/dist/plan-command.js +33 -3
  46. package/dist/plan-command.js.map +1 -1
  47. package/dist/setup-template-scripts.d.ts +5 -3
  48. package/dist/setup-template-scripts.d.ts.map +1 -1
  49. package/dist/setup-template-scripts.js +17 -6
  50. package/dist/setup-template-scripts.js.map +1 -1
  51. package/dist/setup-templates.d.ts +4 -3
  52. package/dist/setup-templates.d.ts.map +1 -1
  53. package/dist/setup-templates.js +24 -13
  54. package/dist/setup-templates.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/admission-fixture.ts +14 -1
  57. package/src/admission.ts +4 -1
  58. package/src/apply-plan-cli.ts +1 -1
  59. package/src/approval-sheet.ts +6 -0
  60. package/src/change-set-contract.ts +51 -1
  61. package/src/existing-declaration-adoption.check.ts +12 -0
  62. package/src/generated/plan-contracts.generated.ts +264 -3
  63. package/src/index.ts +3 -1
  64. package/src/ledger-contract.ts +25 -2
  65. package/src/ledger-trust.ts +5 -0
  66. package/src/materialize.ts +31 -2
  67. package/src/plan-bundle.ts +60 -10
  68. package/src/plan-command.ts +25 -2
  69. package/src/setup-template-scripts.ts +15 -6
  70. package/src/setup-templates.ts +20 -12
@@ -1101,7 +1101,7 @@ export const PLAN_CONTRACTS = {
1101
1101
  "$schema": "http://json-schema.org/draft-07/schema#",
1102
1102
  "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/repository-change-set.json",
1103
1103
  "title": "Repository change set",
1104
- "description": "Issue #1178: everything one pull request would change in one repository when an approved plan is applied, computed from the plan and from observations of the repository's default branch, never from a working tree. @clossys/launcher computes it (planApplyBundle()) and validates it against this file, which its build packs. It is a computed record, not a claim that anything was written: nothing in it says a file exists, a pull request is open, or a change was applied, and none of the repository states (planned, materialized, proposed, applied and so on) appear in it. Those are derived later, from evidence. PUBLIC TEXT. Parts of a set reach the product repository, which may be public, through its files, its ledger, its branch and its pull request, so no member that does is plan or brief text: roles are lowercase id tokens, a planItem is derived from the repository id and the package name (C16), and root names come from a fixed list (C13). The one file that carries plan text by design is clossys/brief.json, whose problem is replaced by a fixed placeholder unless the repository is private. It carries no approval either: which approval binds a set is recorded in the bundle (apply-bundle.json) and in the installed-state ledger (installed-ledger.json), never here, because every member but the six the digest excludes is inside the digest an approval names. No field holds a time, so computing the same set from the same inputs gives the same bytes. Every object is closed: a key this contract does not declare is refused. A map is written as an array of records, because the contract checker implements additionalProperties only as a boolean. Every item act the apply flow will use is declared here, including acts no package computes yet (add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job, exempt-release-age, declare-root-entry, and the agents-pointer and claude-loader records), so a later producer of those acts needs no contract change. DIGEST. changeSetDigest is defined in apply-change-set-digest.md, beside this file, with its corpus apply-change-set-digest.fixture.json. It covers every member except changeSetDigest, branch, bundle, pullRequest, inverse and tooling, and it reduces a derived file to path, mode, derived, item and invariants; that page gives the reason for each exclusion. Because a derived file's bytes are outside the digest, only two files may be derived (code rule C7): the installed-state ledger and the repository's lockfile. PATHS. A path is relative to the repository root, uses / between segments, and has no empty, . or .. segment. A pathAllowList entry is one of the patterns the apply flow may own, listed in definitions.ownedPattern: * matches any characters within one segment, and a segment that is exactly ** matches any number of whole segments, including none; no other segment contains **. DISCOVERY LINKS. A discovery link is a file of mode 120000, a symbolic link as git stores it: its bytes are its target. For a role, it is <root>/clossys-<role> for each discovery root, .claude/skills and .cursor/skills, and its target is ../../.agents/skills/clossys-<role>. REPOSITORY PROFILE. A repository may declare a Controller repository profile (repository-profile.json, or the known alternate name repository-declaration.json; Controller checks governance/repository-profile.json first and otherwise searches the tree for either name). A profile of schema version 3 with a non-empty rootEntries list is a closed vocabulary of the repository's direct children, and Controller's repository-profile check fails on any direct child it does not declare, or declares as prohibited. observed.repositoryProfile records the profile Controller would locate, or null when there is none: its path; rootVocabulary, which is none when the profile has no root vocabulary Controller checks (schema version 1 or 2, or an empty rootEntries), checked when it has one, and unparseable when the profile cannot be read as a profile with a well-formed rootEntries; and, when checked, undeclaredRoots and prohibitedRoots, the root names this set introduces (direct children absent from the default branch) that the vocabulary does not declare, or declares as prohibited. WRITE KINDS. Every whole file an item names has one write kind, fixed by the act (code rule C15), and no act deletes a file: a removal set, when one exists, adds a delete kind with its own rules. write (write-record, compose-skills' SKILL.md and clossys/.state/skills.json, add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job): after is not null; before may be null (create), equal to after (keep), or another digest (update). link (compose-skills' discovery links): after is not null, and before is null or equal to after, because a link's target is fixed by its role. create-or-edit (exempt-release-age): after is not null and differs from before, which may be null (the file is created with only its entry). edit (declare-root-entry): before and after are both not null and differ: the act changes a file the default branch has, and never creates or deletes it. install, pin-starter and write-ledger write no whole file (code rules C4, C7 and C9). LOCKFILE PATH. A repository's lockfile path is observed.lockfile, or, when that is none, the file its package manager writes: package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for yarn. A repository whose packageManager is none has no lockfile path. CANONICAL ORDER. Arrays whose order carries no meaning are written in one order, so the same change always has the same bytes and the same digest. Strings compare by UTF-16 code units, and a pair compares its first member, then its second. CODE RULES, checked in code after the schema passes, because the keywords above cannot relate one field to another. A refusal names the rule and the position of the field at fault, never its value. C1: no two items share an id. C2: every files[].item, keys[].item and refused[].item, and the item of every package invariant, is the id of an item in items. C3: no two files share a path and no two keys share a pointer; paths compare case-insensitively, so two paths that differ only in letter case are the same path. No path is both in files and in refused, and no pointer is both in keys and in refused. Every path the set touches is matched by some pathAllowList entry: every files[].path, the file of every keys[] entry, and the path of every item that names one (exempt-release-age). C4: ledger.generation is 0 or more; exactly one item has act write-ledger, and exactly one file names it: a derived file at clossys/.state/installed.json whose invariants are exactly one ledgerGeneration equal to ledger.generation plus 1. C5: changeSetDigest is the digest apply-change-set-digest.md defines, branch is clossys/apply- followed by the first 12 hexadecimal digits of changeSetDigest, and pullRequest.title ends with the same 12 digits. C6: no planItem appears twice across items and deferred. C7: every derived file is either the ledger (C4) or the lockfile: at most one derived file is at the repository's lockfile path (see LOCKFILE PATH), and every one of its invariants is a package invariant; no other file is derived, and no whole file is at the ledger path, at package.json, or at any lockfile name. C8: items are sorted by id; files by path; keys by file, then pointer; deferred by planItem; each file's invariants by package name; and, each with no entry repeated, refused by path (or file), then pointer (a path refusal has none, which sorts first), pathAllowList by value, observed.releaseAgeSurfaces by surface, then path, observed.symlinkedSkillRoots by value, and tooling by tool. C9: each item's writes match the item. A file naming an item that is not an install, pin-starter or write-ledger item is a whole file. A whole file has mode 100644, except a discovery link, which has mode 120000, and a discovery link's after, unless null, is the content digest of its target text, with no line feed. Each of the following items is named by exactly one whole file or one path refusal at each path it binds, and by nothing else: a write-record item binds clossys/brief.json for source engagement-brief, AGENTS.md for agents-pointer, CLAUDE.md for claude-loader (by path only: the pointer and loader files' bytes are not checked here), and clossys/AGENTS.md for agents-guide (by path only: the step that writes and verifies the file checks its bytes); a compose-skills item names each of its roles once, with no role repeated, and binds, for each role, .agents/skills/clossys-<role>/SKILL.md, and, for each role whose SKILL.md is named by a whole file, the discovery link under each discovery root that observed.symlinkedSkillRoots does not list, and binds clossys/.state/skills.json once (a role whose SKILL.md is refused gets no discovery link, because a link would expose a skill the flow does not own); an add-caller-workflow item binds .github/workflows/clossys-adoption-evidence.yml, .github/workflows/clossys-adoption-decision.yml and .github/scripts/clossys-collect-adoption-snapshot.mjs; a write-starter-request item binds .starter/request.json; an add-ci-template item binds .github/workflows/clossys-ci.yml; and an add-path-scope-job item binds .github/workflows/clossys-path-scope.yml. An exempt-release-age item is named by at most one whole file or path refusal, at its own path, and by nothing else: by none when the default branch already lists its entry (which needs the file's bytes, so it is not checked here). A declare-root-entry item is named by exactly one whole file or path refusal, at its own path, and by nothing else. The write-ledger item is named by no refusal: the ledger is always written. So every refusal names a path or key its item binds, and no other: a path refusal only at a path the item binds above, and a key refusal only for an install or pin-starter item, at /dependencies/ or /devDependencies/ and that item's package name. No key and no key refusal names an item that is not an install or pin-starter item. An install or pin-starter item is never named by a whole file or a path refusal, and a pin-starter item's placement is devDependencies. Every keys[] entry and every package invariant names such an item: a key's pointer is /<placement>/<name with / written ~1> and its after is the item's version; an invariant's name, version and integrity are the item's. An item with satisfiedInBase true is named by no key, invariant or refusal; one with satisfiedInBase false is named either by exactly one key and exactly one invariant and no refusal, or by at least one key refusal and no key or invariant. A derived lockfile's item is the item of its first invariant. C10: a setup set has no install item (an install waits in deferred), an apply set defers nothing, and a set has at most one pin-starter item. C11: a setup set has exactly one item of each act add-caller-workflow, write-starter-request, add-ci-template and add-path-scope-job, exactly one pin-starter item, and exactly one exempt-release-age item when observed.packageManager is pnpm or yarn, and none otherwise, because npm has no key that exempts a scope from its release-age window. C12: an exempt-release-age item's surface is pnpm-workspace with path pnpm-workspace.yaml in a repository whose observed.packageManager is pnpm, or yarnrc with path .yarnrc.yml in one whose packageManager is yarn; and its scope is the publishing scope the validating package packs from this repository's package-scope.json. This contract sees content digests, not bytes, so it does not check that the file an exempt-release-age item writes differs from its before only by that one entry; that check needs the file's bytes and belongs to the step that writes them. C13: a set has at most one declare-root-entry item, and has one exactly when observed.repositoryProfile is not null and its rootVocabulary is unparseable, or is checked with a non-empty undeclaredRoots or prohibitedRoots. Its path is observed.repositoryProfile.path, and its entries' names are exactly undeclaredRoots, in that order. When rootVocabulary is unparseable, its entries are empty and it is named by a path refusal with reason root-vocabulary-unknown; when prohibitedRoots is not empty, it is named by a path refusal with reason root-entry-prohibited, because the flow never overrides a name the repository prohibits; otherwise it is named by a whole file (whose write kind, edit, C15 checks). undeclaredRoots and prohibitedRoots are empty unless rootVocabulary is checked, share no name, and each name in them is the first segment of a path the set creates: a whole file whose before is null, or a derived file. A refused path is not written, and a key's file or an edited file already exists on the default branch, so none of them introduces a root name. Each of those names, and each entry's name, is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), so it is a fixed name no plan or brief text can supply, and well within Controller's 255 UTF-16 code units. When no declare-root-entry item is needed, as when the profile already declares every root name the set introduces, the set has none. This contract sees content digests, not bytes, so it does not check that the profile's after differs from its before only by those entries; that check belongs to the step that writes them. C14: every path in observed.linkedAgentsPaths is .agents, .agents/skills, or .agents/skills/clossys-<role> for a role of a compose-skills item; a SKILL.md at or under such a path is never written, and is named by a path refusal with reason skills-root-is-link; and no other path refusal has that reason. A write through a symbolic link would land wherever it points. observed.linkedAgentsPaths covers .agents only: a symbolic link at another root the set writes under, such as clossys/, .github/ or .starter/, is refused by the step that writes the files, which this contract does not describe. C15: every whole file obeys its item's write kind (WRITE KINDS). C16: every package item's planItem is the repository id, a colon and the package name, `${repository.id}:${package.name}`, exactly and in the same letter case, and every deferral's planItem is the repository id, a colon and a package name; a planItem is written into the installed-state ledger, which may be public, so it is never free text.",
1104
+ "description": "Issue #1178: everything one pull request would change in one repository when an approved plan is applied, computed from the plan and from observations of the repository's default branch, never from a working tree. @clossys/launcher computes it (planApplyBundle()) and validates it against this file, which its build packs. It is a computed record, not a claim that anything was written: nothing in it says a file exists, a pull request is open, or a change was applied, and none of the repository states (planned, materialized, proposed, applied and so on) appear in it. Those are derived later, from evidence. PUBLIC TEXT. Parts of a set reach the product repository, which may be public, through its files, its ledger, its branch and its pull request, so no member that does is plan or brief text: roles are lowercase id tokens, a planItem is derived from the repository id and the package name (C16), and root names come from a fixed list (C13). The one file that carries plan text by design is clossys/brief.json, whose problem is replaced by a fixed placeholder unless the repository is private. It carries no approval either: which approval binds a set is recorded in the bundle (apply-bundle.json) and in the installed-state ledger (installed-ledger.json), never here, because every member but the six the digest excludes is inside the digest an approval names. No field holds a time, so computing the same set from the same inputs gives the same bytes. Every object is closed: a key this contract does not declare is refused. A map is written as an array of records, because the contract checker implements additionalProperties only as a boolean. Every item act the apply flow will use is declared here, including acts no package computes yet (add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job, exempt-release-age, declare-root-entry, and the agents-pointer and claude-loader records), so a later producer of those acts needs no contract change. DIGEST. changeSetDigest is defined in apply-change-set-digest.md, beside this file, with its corpus apply-change-set-digest.fixture.json. It covers every member except changeSetDigest, branch, bundle, pullRequest, inverse and tooling, and it reduces a derived file to path, mode, derived, item and invariants; that page gives the reason for each exclusion. Because a derived file's bytes are outside the digest, only two files may be derived (code rule C7): the installed-state ledger and the repository's lockfile. PATHS. A path is relative to the repository root, uses / between segments, and has no empty, . or .. segment. A pathAllowList entry is one of the patterns the apply flow may own, listed in definitions.ownedPattern: * matches any characters within one segment, and a segment that is exactly ** matches any number of whole segments, including none; no other segment contains **. DISCOVERY LINKS. A discovery link is a file of mode 120000, a symbolic link as git stores it: its bytes are its target. For a role, it is <root>/clossys-<role> for each discovery root, .claude/skills and .cursor/skills, and its target is ../../.agents/skills/clossys-<role>. REPOSITORY PROFILE. A repository may declare a Controller repository profile (repository-profile.json, or the known alternate name repository-declaration.json; Controller checks governance/repository-profile.json first and otherwise searches the tree for either name). A profile of schema version 3 with a non-empty rootEntries list is a closed vocabulary of the repository's direct children, and Controller's repository-profile check fails on any direct child it does not declare, or declares as prohibited. observed.repositoryProfile records the profile Controller would locate, or null when there is none: its path; rootVocabulary, which is none when the profile has no root vocabulary Controller checks (schema version 1 or 2, or an empty rootEntries), checked when it has one, and unparseable when the profile cannot be read as a profile with a well-formed rootEntries; and, when checked, undeclaredRoots and prohibitedRoots, the root names this set introduces (direct children absent from the default branch) that the vocabulary does not declare, or declares as prohibited. WRITE KINDS. Every whole file an item names has one write kind, fixed by the act (code rule C15), and no act deletes a file: a removal set, when one exists, adds a delete kind with its own rules. write (write-record, compose-skills' SKILL.md and clossys/.state/skills.json, add-caller-workflow, write-starter-request, add-ci-template, add-path-scope-job): after is not null; before may be null (create), equal to after (keep), or another digest (update). link (compose-skills' discovery links): after is not null, and before is null or equal to after, because a link's target is fixed by its role. create-or-edit (exempt-release-age): after is not null and differs from before, which may be null (the file is created with only its entry). edit (declare-root-entry): before and after are both not null and differ: the act changes a file the default branch has, and never creates or deletes it. install, pin-starter and write-ledger write no whole file (code rules C4, C7 and C9). LOCKFILE PATH. A repository's lockfile path is observed.lockfile, or, when that is none, the file its package manager writes: package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for yarn. A repository whose packageManager is none has no lockfile path. CANONICAL ORDER. Arrays whose order carries no meaning are written in one order, so the same change always has the same bytes and the same digest. Strings compare by UTF-16 code units, and a pair compares its first member, then its second. CODE RULES, checked in code after the schema passes, because the keywords above cannot relate one field to another. A refusal names the rule and the position of the field at fault, never its value. C1: no two items share an id. C2: every files[].item, keys[].item and refused[].item, and the item of every package invariant, is the id of an item in items. C3: no two files share a path and no two keys share a pointer; paths compare case-insensitively, so two paths that differ only in letter case are the same path. No path is both in files and in refused, and no pointer is both in keys and in refused. Every path the set touches is matched by some pathAllowList entry: every files[].path, the file of every keys[] entry, and the path of every item that names one (exempt-release-age). C4: ledger.generation is 0 or more; exactly one item has act write-ledger, and exactly one file names it: a derived file at clossys/.state/installed.json whose invariants are exactly one ledgerGeneration equal to ledger.generation plus 1. C5: changeSetDigest is the digest apply-change-set-digest.md defines, branch is the covered agentProvenance namespace (codex, claude or cursor), or clossys when omitted, followed by /apply- and the first 12 hexadecimal digits of changeSetDigest, and pullRequest.title ends with the same 12 digits. C6: no planItem appears twice across items and deferred. C7: every derived file is either the ledger (C4) or the lockfile: at most one derived file is at the repository's lockfile path (see LOCKFILE PATH), and every one of its invariants is a package invariant; no other file is derived, and no whole file is at the ledger path, at package.json, or at any lockfile name. C8: items are sorted by id; files by path; keys by file, then pointer; deferred by planItem; each file's invariants by package name; and, each with no entry repeated, refused by path (or file), then pointer (a path refusal has none, which sorts first), pathAllowList by value, observed.releaseAgeSurfaces by surface, then path, observed.symlinkedSkillRoots by value, and tooling by tool. C9: each item's writes match the item. A file naming an item that is not an install, pin-starter or write-ledger item is a whole file. A whole file has mode 100644, except a discovery link, which has mode 120000, and a discovery link's after, unless null, is the content digest of its target text, with no line feed. Each of the following items is named by exactly one whole file or one path refusal at each path it binds, and by nothing else: a write-record item binds clossys/brief.json for source engagement-brief, AGENTS.md for agents-pointer, CLAUDE.md for claude-loader (by path only: the pointer and loader files' bytes are not checked here), and clossys/AGENTS.md for agents-guide (by path only: the step that writes and verifies the file checks its bytes); a compose-skills item names each of its roles once, with no role repeated, and binds, for each role, .agents/skills/clossys-<role>/SKILL.md, and, for each role whose SKILL.md is named by a whole file, the discovery link under each discovery root that observed.symlinkedSkillRoots does not list, and binds clossys/.state/skills.json once (a role whose SKILL.md is refused gets no discovery link, because a link would expose a skill the flow does not own); an add-caller-workflow item binds .github/workflows/clossys-adoption-evidence.yml, .github/workflows/clossys-adoption-decision.yml and .github/scripts/clossys-collect-adoption-snapshot.mjs; a write-starter-request item binds .starter/request.json; an add-ci-template item binds .github/workflows/clossys-ci.yml; and an add-path-scope-job item binds .github/workflows/clossys-path-scope.yml. An exempt-release-age item is named by at most one whole file or path refusal, at its own path, and by nothing else: by none when the default branch already lists its entry (which needs the file's bytes, so it is not checked here). A declare-root-entry item is named by exactly one whole file or path refusal, at its own path, and by nothing else. The write-ledger item is named by no refusal: the ledger is always written. So every refusal names a path or key its item binds, and no other: a path refusal only at a path the item binds above, and a key refusal only for an install or pin-starter item, at /dependencies/ or /devDependencies/ and that item's package name. No key and no key refusal names an item that is not an install or pin-starter item. An install or pin-starter item is never named by a whole file or a path refusal, and a pin-starter item's placement is devDependencies. Every keys[] entry and every package invariant names such an item: a key's pointer is /<placement>/<name with / written ~1> and its after is the item's version; an invariant's name, version and integrity are the item's. An item with satisfiedInBase true is named by no key, invariant or refusal; one with satisfiedInBase false is named either by exactly one key and exactly one invariant and no refusal, or by at least one key refusal and no key or invariant. A derived lockfile's item is the item of its first invariant. C10: a setup set has no install item (an install waits in deferred), an apply set defers nothing, and a set has at most one pin-starter item. C11: a setup set has exactly one item of each act add-caller-workflow, write-starter-request, add-ci-template and add-path-scope-job, exactly one pin-starter item, and exactly one exempt-release-age item when observed.packageManager is pnpm or yarn, and none otherwise, because npm has no key that exempts a scope from its release-age window. C12: an exempt-release-age item's surface is pnpm-workspace with path pnpm-workspace.yaml in a repository whose observed.packageManager is pnpm, or yarnrc with path .yarnrc.yml in one whose packageManager is yarn; and its scope is the publishing scope the validating package packs from this repository's package-scope.json. This contract sees content digests, not bytes, so it does not check that the file an exempt-release-age item writes differs from its before only by that one entry; that check needs the file's bytes and belongs to the step that writes them. C13: a set has at most one declare-root-entry item, and has one exactly when observed.repositoryProfile is not null and its rootVocabulary is unparseable, or is checked with a non-empty undeclaredRoots or prohibitedRoots. Its path is observed.repositoryProfile.path, and its entries' names are exactly undeclaredRoots, in that order. When rootVocabulary is unparseable, its entries are empty and it is named by a path refusal with reason root-vocabulary-unknown; when prohibitedRoots is not empty, it is named by a path refusal with reason root-entry-prohibited, because the flow never overrides a name the repository prohibits; otherwise it is named by a whole file (whose write kind, edit, C15 checks). undeclaredRoots and prohibitedRoots are empty unless rootVocabulary is checked, share no name, and each name in them is the first segment of a path the set creates: a whole file whose before is null, or a derived file. A refused path is not written, and a key's file or an edited file already exists on the default branch, so none of them introduces a root name. Each of those names, and each entry's name, is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), so it is a fixed name no plan or brief text can supply, and well within Controller's 255 UTF-16 code units. When no declare-root-entry item is needed, as when the profile already declares every root name the set introduces, the set has none. This contract sees content digests, not bytes, so it does not check that the profile's after differs from its before only by those entries; that check belongs to the step that writes them. C14: every path in observed.linkedAgentsPaths is .agents, .agents/skills, or .agents/skills/clossys-<role> for a role of a compose-skills item; a SKILL.md at or under such a path is never written, and is named by a path refusal with reason skills-root-is-link; and no other path refusal has that reason. A write through a symbolic link would land wherever it points. observed.linkedAgentsPaths covers .agents only: a symbolic link at another root the set writes under, such as clossys/, .github/ or .starter/, is refused by the step that writes the files, which this contract does not describe. C15: every whole file obeys its item's write kind (WRITE KINDS). C16: every package item's planItem is the repository id, a colon and the package name, `${repository.id}:${package.name}`, exactly and in the same letter case, and every deferral's planItem is the repository id, a colon and a package name; a planItem is written into the installed-state ledger, which may be public, so it is never free text. Existing-declaration adoption: optional proof rows are covered by setup change-set approval, rendered with that setup digest into the protected ledger, and preserved exactly by the admitted apply generation. Only same-name, same-placement root declarations are eligible. Setup consumes pin-starter; install rows bind deferred identities, which the next admitted apply consumes. Prior literal declaration and resolved registry identity must be independently checked against protected source data, and desired identity against head data; metadata does not prove installed bytes. All default refusal paths remain.",
1105
1105
  "type": "object",
1106
1106
  "additionalProperties": false,
1107
1107
  "required": [
@@ -1218,9 +1218,18 @@ export const PLAN_CONTRACTS = {
1218
1218
  "$ref": "#/definitions/ownedPattern"
1219
1219
  }
1220
1220
  },
1221
+ "agentProvenance": {
1222
+ "type": "string",
1223
+ "enum": [
1224
+ "codex",
1225
+ "claude",
1226
+ "cursor"
1227
+ ],
1228
+ "description": "Optional authoring agent namespace. Covered by the digest; omission preserves the legacy clossys branch namespace."
1229
+ },
1221
1230
  "branch": {
1222
1231
  "type": "string",
1223
- "pattern": "^clossys/apply-[0-9a-f]{12}$",
1232
+ "pattern": "^(clossys|codex|claude|cursor)/apply-[0-9a-f]{12}$",
1224
1233
  "description": "The branch this set is proposed from. Excluded from the digest: it is a function of it (code rule C5)."
1225
1234
  },
1226
1235
  "bundle": {
@@ -1299,6 +1308,14 @@ export const PLAN_CONTRACTS = {
1299
1308
  "changeSetDigest": {
1300
1309
  "$ref": "advisor-plan.json#/definitions/sha256Digest",
1301
1310
  "description": "This set's digest (apply-change-set-digest.md; code rule C5)."
1311
+ },
1312
+ "existingDeclarationAdoptions": {
1313
+ "type": "array",
1314
+ "minItems": 1,
1315
+ "items": {
1316
+ "$ref": "#/definitions/existingDeclarationAdoption"
1317
+ },
1318
+ "description": "Explicit root declaration consent covered by the approved setup. Resolution evidence is metadata, not installed-byte proof. Omission preserves legacy behavior."
1302
1319
  }
1303
1320
  },
1304
1321
  "definitions": {
@@ -2062,6 +2079,122 @@ export const PLAN_CONTRACTS = {
2062
2079
  "title": "a path whose last segment is repository-profile.json or repository-declaration.json, under none of the directories Controller's profile search skips (.git, node_modules, dist, build, .next, coverage, .turbo, .cache), and not under .github/workflows/, where the flow owns only clossys-* workflows",
2063
2080
  "type": "string",
2064
2081
  "pattern": "^(?!(?:[^/]*/)*(?:\\.git|node_modules|dist|build|\\.next|coverage|\\.turbo|\\.cache)/)(?!\\.github/workflows/)(?:[^/]+/)*repository-(?:profile|declaration)\\.json$"
2082
+ },
2083
+ "adoptionResolution": {
2084
+ "type": "object",
2085
+ "additionalProperties": false,
2086
+ "required": [
2087
+ "name",
2088
+ "version",
2089
+ "integrity"
2090
+ ],
2091
+ "properties": {
2092
+ "name": {
2093
+ "type": "string",
2094
+ "pattern": "^@clossys/[a-z0-9-]+$"
2095
+ },
2096
+ "version": {
2097
+ "type": "string",
2098
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
2099
+ },
2100
+ "integrity": {
2101
+ "type": "string",
2102
+ "pattern": "^sha512-[A-Za-z0-9+/]{86}==$"
2103
+ }
2104
+ }
2105
+ },
2106
+ "adoptionDesired": {
2107
+ "type": "object",
2108
+ "additionalProperties": false,
2109
+ "required": [
2110
+ "name",
2111
+ "version",
2112
+ "integrity",
2113
+ "planItem",
2114
+ "act",
2115
+ "placement"
2116
+ ],
2117
+ "properties": {
2118
+ "name": {
2119
+ "type": "string",
2120
+ "pattern": "^@clossys/[a-z0-9-]+$"
2121
+ },
2122
+ "version": {
2123
+ "type": "string",
2124
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
2125
+ },
2126
+ "integrity": {
2127
+ "type": "string",
2128
+ "pattern": "^sha512-[A-Za-z0-9+/]{86}==$"
2129
+ },
2130
+ "planItem": {
2131
+ "type": "string"
2132
+ },
2133
+ "act": {
2134
+ "enum": [
2135
+ "install",
2136
+ "pin-starter"
2137
+ ]
2138
+ },
2139
+ "placement": {
2140
+ "enum": [
2141
+ "dependencies",
2142
+ "devDependencies"
2143
+ ]
2144
+ }
2145
+ }
2146
+ },
2147
+ "existingDeclarationAdoption": {
2148
+ "type": "object",
2149
+ "additionalProperties": false,
2150
+ "required": [
2151
+ "file",
2152
+ "placement",
2153
+ "name",
2154
+ "beforeVersion",
2155
+ "beforeResolved",
2156
+ "desired",
2157
+ "observedBaseCommit",
2158
+ "consent",
2159
+ "desiredSnapshotDigest"
2160
+ ],
2161
+ "properties": {
2162
+ "file": {
2163
+ "const": "package.json"
2164
+ },
2165
+ "placement": {
2166
+ "enum": [
2167
+ "dependencies",
2168
+ "devDependencies"
2169
+ ]
2170
+ },
2171
+ "name": {
2172
+ "type": "string",
2173
+ "pattern": "^@clossys/[a-z0-9-]+$"
2174
+ },
2175
+ "beforeVersion": {
2176
+ "description": "Stable exact, caret or tilde registry declaration; beforeResolved.version must satisfy this bounded declaration.",
2177
+ "type": "string",
2178
+ "pattern": "^[~^]?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
2179
+ },
2180
+ "beforeResolved": {
2181
+ "$ref": "#/definitions/adoptionResolution"
2182
+ },
2183
+ "desired": {
2184
+ "$ref": "#/definitions/adoptionDesired"
2185
+ },
2186
+ "observedBaseCommit": {
2187
+ "type": "string",
2188
+ "pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$"
2189
+ },
2190
+ "consent": {
2191
+ "const": "adopt-existing-declaration"
2192
+ },
2193
+ "desiredSnapshotDigest": {
2194
+ "type": "string",
2195
+ "pattern": "^sha256:[0-9a-f]{64}$"
2196
+ }
2197
+ }
2065
2198
  }
2066
2199
  }
2067
2200
  },
@@ -2337,7 +2470,7 @@ export const PLAN_CONTRACTS = {
2337
2470
  "$schema": "http://json-schema.org/draft-07/schema#",
2338
2471
  "$id": "https://github.com/clossys/foundry/blob/main/docs/contracts/installed-ledger.json",
2339
2472
  "title": "Installed-state ledger",
2340
- "description": "Issue #1178: the installed-state ledger, clossys/.state/installed.json in a product repository. It records what the apply flow wrote there, one generation per merged change set, and on whose approval each generation was written. Every change set writes the next generation as a derived file (repository-change-set.json code rules C4 and C7), so the ledger changes only through a pull request the client merges. @clossys/launcher validates a ledger against this file, which its build packs; nothing writes one yet. Every object is closed: a key this contract does not declare is refused. The contract is self-contained so a package can pack it alone: the definitions it shares with advisor-plan.json and repository-change-set.json are copies, and Launcher's tests hold each copy equal to its source. PUBLIC SURFACE. The ledger is committed to the product repository, which may be public, so it holds only digests, versions, integrity values, paths the flow owns, and this repository's own id -- never a sibling repository's id, never plan or brief prose, never a person. No string in it is plan or brief text: a planItem is exactly this repository's id, a colon and the package name (code rule L10), a role appears only as a lowercase id token in a skill path (L5), and a root entry is one of the fixed names an owned pattern can introduce (L9). TRUST. Passing this contract makes a ledger well formed, not true: it is a claim anyone with write access could have edited. A reader that holds the hub trusts a row only when the change set the row names is one the hub holds as a self-verifying file (clossys/.state/apply/change-sets/<digest>.json whose recomputed changeSetDigest equals its name) and, for a files row, that change set's files hold the row's path with the row's after; a row that fails is ignored as unowned (ledger-foreign-row). The reader also requires every history[].changeSet to be such a file (else ledger-chain) and repository.nodeId to equal the observed repository's immutable id (else identity). A reader without the hub, such as a product repository's own CI, trusts no row this way; it may only compare a pull request's ledger with its base's, under SUCCESSION below, where the base is authenticated by the client's merge into its protected default branch. BINDING. history[].binding says on what authority each generation was written. approved: the change set was a member of the bundle the founder approved; subjectDigest is the approving decision's subjectDigest (advisor-plan.json, APPROVAL BINDING). It need not equal the entry's bundle: bundle records the run that computed the set, and a later run's bundle differs whenever a sibling repository's set moved on, while the set itself is still a member of the approved bundle. admitted: an apply set written under the one-approval rule with no second approval; subjectDigest is the approved bundle's digest, and setupChangeSet is the setup set this apply set follows. Neither is inside any digest: the ledger is a derived file, outside its own change set's digest (apply-change-set-digest.md), so it can name the digests an approval binds without containing its own result, and no whole file carries a binding. RENDER. The ledger a change set X, with digest d and binding b, writes over the previous ledger P (none at generation 0) is: repository from X.repository's id and nodeId; generation X.ledger.generation plus 1; history P's history followed by { generation, changeSet: d, phase: X.phase, planDigest: X.planDigest, bundle: X.bundle, baseCommit: X.repository.baseCommit, binding: b }; files P's rows, then for each whole file of X: after null removes the row at its path, a file whose before equals its after keeps P's row at that path unchanged when P has one with that after, and every other file sets the row { path, mode, after, changeSet: d }. In an apply set the only file with a before of null that RENDER writes a row for is the Launcher guide (path clossys/AGENTS.md, mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53, the digest of the guide's bytes), and only where a previous ledger P exists and holds no row at that path in any letter case, for an install set up before the guide existed; an apply set's add at any other path, with any other mode or after, with no previous ledger, or at that path where P holds a row, is refused, so no ledger is rendered for it. A row for a file the default branch already had before the flow edited it -- the Controller profile, or a release-age surface -- is a compare-and-swap record of the bytes the flow last wrote, not ownership of the whole file: the flow owns only the entries rows it added there; keys P's rows, then for each key of X: after null removes the row at its pointer, any other value sets { file, pointer, value: after, changeSet: d }; entries P's rows, then for each exempt-release-age item of X that no path refusal names, the row { file: the item's path, key: minimumReleaseAgeExclude for pnpm-workspace or npmPreapprovedPackages for yarnrc, value: the item's scope and /*, changeSet: d }, keeping P's row unchanged when it already holds that file, key and value, and for each declare-root-entry item of X that no path refusal names, one row per entry { file: the item's path, key: rootEntries, value: the entry's name, changeSet: d }, keeping P's identical row unchanged; packages P's rows, then for each install or pin-starter item of X that no key refusal names, the row { planItem, act, name, version, integrity, placement, changeSet: d } replacing any row with that planItem or that name, or P's row unchanged when it already has that planItem and identity; deferred exactly X's deferred, each with the plan's identity for that planItem ({ planItem, act, name, version, integrity, placement, reason, changeSet: d }). An item satisfied in the base is recorded in packages like any other, because packages lists the package acts in effect, and keys lists only the keys the flow wrote. No row names the ledger or the lockfile. Arrays are written in the canonical order of code rule L8, and every object's members in the order this contract declares them, at every depth. The bytes are the UTF-8 of JSON.stringify(ledger, null, 2) and one line feed. The corpus installed-ledger.fixture.json, beside this file, holds ledgers with the SHA-256 of these bytes, computed independently of any package. CODE RULES, checked in code after the schema passes. A refusal names the rule and the position of the field at fault, never its value. L1: generation is 1 or more and equals the number of history entries, and history[i].generation is i plus 1. L2: no two history entries name the same changeSet, so no two share a bundle and changeSet pair either: a change set is written once. L3: a setup entry's binding is approved; an admitted binding is on an apply entry that is not the first, and the entry immediately before it is the one it names: that entry's changeSet equals setupChangeSet, its phase is setup, its binding is approved, and it has the same planDigest and the same subjectDigest. L4: every row's changeSet, in files, keys, entries, packages and deferred, is the changeSet of some history entry; every deferred row's changeSet is the last history entry's; and when the last entry's phase is apply, deferred is empty. L5: no files row is at the ledger's own path, at package.json or at a lockfile name (package-lock.json, pnpm-lock.yaml, yarn.lock), compared case-insensitively; every files row's path is matched by some definitions.ownedPattern entry, where * matches any characters within one segment and a segment that is exactly ** matches any number of whole segments; a files row has mode 120000 exactly when its path is a discovery link, .claude/skills/clossys-<role> or .cursor/skills/clossys-<role> with <role> one segment; and the <role> of every path under .agents/skills/clossys-<role>/ and of every discovery link is a lowercase id token. L6: every keys row's pointer is /<placement>/<name with ~ written ~0 and / written ~1> of exactly one packages row, and its value is that row's version. L7: no planItem and no package name appears twice across packages and deferred together. L8: files are sorted by path, keys by file then pointer, entries by file, then key, then value, packages by planItem and deferred by planItem, strings comparing by UTF-16 code units, with no entry repeated; files paths compare case-insensitively for repetition. L9: every rootEntries row names the same file, the one Controller profile the flow edits, and each value is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), well within Controller's 255 UTF-16 code units. L10: every packages and deferred row's planItem is exactly repository.id, a colon and the row's name, in the same letter case. SUCCESSION, checked by comparing a pull request's head ledger with its base's ledger (the base's is absent before generation 1), each read from its bytes: each must be valid under this contract, and its bytes must be exactly the bytes RENDER gives it, so a repeated key, a byte order mark or any other spelling of a ledger is refused (rule bytes), never read as an unchanged ledger. S1: when head's bytes equal base's, the pull request makes no ledger change, and the rules below do not apply. S2: otherwise head.repository equals base.repository, head.generation is base.generation plus 1 (1 when the base has none), and head.history without its last entry equals base.history. S3: when head's last entry is admitted, the base has a ledger and its last history entry is the one setupChangeSet names; head.deferred is empty; head.entries equal base's, and head.files equal base's or add exactly one row and drop none, the row for clossys/AGENTS.md with mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53 (the digest of the Launcher guide's bytes, so a row naming any other bytes is refused) and head's last changeSet, where the base holds no row at that path in any letter case; every base keys row and every base packages row is in head unchanged; and the other packages rows are exactly base.deferred's rows, each with the same planItem, act, name, version, integrity and placement and head's last changeSet; and the other keys rows each name one of those packages, with head's last changeSet. So an admitted generation installs exactly the packages the approved setup set deferred and changes no other row, apart from that one files row. An approved generation is checked by S2 alone, and it proves nothing: a reader without the hub cannot authenticate the approval it names, so for that reader an approved head generation is an unauthenticated claim of approval, never an admission and never a pass. A pull request could relabel an admitted generation approved to escape S3; a reader that must decide without the hub refuses such a head on an apply pull request, or treats the pull request as one that needs the client's own review. The result of a succession check therefore says which of three it found: no new generation, an admitted generation that S3 proved, or a claimed approval.",
2473
+ "description": "Issue #1178: the installed-state ledger, clossys/.state/installed.json in a product repository. It records what the apply flow wrote there, one generation per merged change set, and on whose approval each generation was written. Every change set writes the next generation as a derived file (repository-change-set.json code rules C4 and C7), so the ledger changes only through a pull request the client merges. @clossys/launcher validates a ledger against this file, which its build packs; nothing writes one yet. Every object is closed: a key this contract does not declare is refused. The contract is self-contained so a package can pack it alone: the definitions it shares with advisor-plan.json and repository-change-set.json are copies, and Launcher's tests hold each copy equal to its source. PUBLIC SURFACE. The ledger is committed to the product repository, which may be public, so it holds only digests, versions, integrity values, paths the flow owns, and this repository's own id -- never a sibling repository's id, never plan or brief prose, never a person. No string in it is plan or brief text: a planItem is exactly this repository's id, a colon and the package name (code rule L10), a role appears only as a lowercase id token in a skill path (L5), and a root entry is one of the fixed names an owned pattern can introduce (L9). TRUST. Passing this contract makes a ledger well formed, not true: it is a claim anyone with write access could have edited. A reader that holds the hub trusts a row only when the change set the row names is one the hub holds as a self-verifying file (clossys/.state/apply/change-sets/<digest>.json whose recomputed changeSetDigest equals its name) and, for a files row, that change set's files hold the row's path with the row's after; a row that fails is ignored as unowned (ledger-foreign-row). The reader also requires every history[].changeSet to be such a file (else ledger-chain) and repository.nodeId to equal the observed repository's immutable id (else identity). A reader without the hub, such as a product repository's own CI, trusts no row this way; it may only compare a pull request's ledger with its base's, under SUCCESSION below, where the base is authenticated by the client's merge into its protected default branch. BINDING. history[].binding says on what authority each generation was written. approved: the change set was a member of the bundle the founder approved; subjectDigest is the approving decision's subjectDigest (advisor-plan.json, APPROVAL BINDING). It need not equal the entry's bundle: bundle records the run that computed the set, and a later run's bundle differs whenever a sibling repository's set moved on, while the set itself is still a member of the approved bundle. admitted: an apply set written under the one-approval rule with no second approval; subjectDigest is the approved bundle's digest, and setupChangeSet is the setup set this apply set follows. Neither is inside any digest: the ledger is a derived file, outside its own change set's digest (apply-change-set-digest.md), so it can name the digests an approval binds without containing its own result, and no whole file carries a binding. RENDER. The ledger a change set X, with digest d and binding b, writes over the previous ledger P (none at generation 0) is: repository from X.repository's id and nodeId; generation X.ledger.generation plus 1; history P's history followed by { generation, changeSet: d, phase: X.phase, planDigest: X.planDigest, bundle: X.bundle, baseCommit: X.repository.baseCommit, binding: b }; files P's rows, then for each whole file of X: after null removes the row at its path, a file whose before equals its after keeps P's row at that path unchanged when P has one with that after, and every other file sets the row { path, mode, after, changeSet: d }. In an apply set the only file with a before of null that RENDER writes a row for is the Launcher guide (path clossys/AGENTS.md, mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53, the digest of the guide's bytes), and only where a previous ledger P exists and holds no row at that path in any letter case, for an install set up before the guide existed; an apply set's add at any other path, with any other mode or after, with no previous ledger, or at that path where P holds a row, is refused, so no ledger is rendered for it. A row for a file the default branch already had before the flow edited it -- the Controller profile, or a release-age surface -- is a compare-and-swap record of the bytes the flow last wrote, not ownership of the whole file: the flow owns only the entries rows it added there; keys P's rows, then for each key of X: after null removes the row at its pointer, any other value sets { file, pointer, value: after, changeSet: d }; entries P's rows, then for each exempt-release-age item of X that no path refusal names, the row { file: the item's path, key: minimumReleaseAgeExclude for pnpm-workspace or npmPreapprovedPackages for yarnrc, value: the item's scope and /*, changeSet: d }, keeping P's row unchanged when it already holds that file, key and value, and for each declare-root-entry item of X that no path refusal names, one row per entry { file: the item's path, key: rootEntries, value: the entry's name, changeSet: d }, keeping P's identical row unchanged; packages P's rows, then for each install or pin-starter item of X that no key refusal names, the row { planItem, act, name, version, integrity, placement, changeSet: d } replacing any row with that planItem or that name, or P's row unchanged when it already has that planItem and identity; deferred exactly X's deferred, each with the plan's identity for that planItem ({ planItem, act, name, version, integrity, placement, reason, changeSet: d }). An item satisfied in the base is recorded in packages like any other, because packages lists the package acts in effect, and keys lists only the keys the flow wrote. No row names the ledger or the lockfile. Arrays are written in the canonical order of code rule L8, and every object's members in the order this contract declares them, at every depth. The bytes are the UTF-8 of JSON.stringify(ledger, null, 2) and one line feed. The corpus installed-ledger.fixture.json, beside this file, holds ledgers with the SHA-256 of these bytes, computed independently of any package. CODE RULES, checked in code after the schema passes. A refusal names the rule and the position of the field at fault, never its value. L1: generation is 1 or more and equals the number of history entries, and history[i].generation is i plus 1. L2: no two history entries name the same changeSet, so no two share a bundle and changeSet pair either: a change set is written once. L3: a setup entry's binding is approved; an admitted binding is on an apply entry that is not the first, and the entry immediately before it is the one it names: that entry's changeSet equals setupChangeSet, its phase is setup, its binding is approved, and it has the same planDigest and the same subjectDigest. L4: every row's changeSet, in files, keys, entries, packages and deferred, is the changeSet of some history entry; every deferred row's changeSet is the last history entry's; and when the last entry's phase is apply, deferred is empty. L5: no files row is at the ledger's own path, at package.json or at a lockfile name (package-lock.json, pnpm-lock.yaml, yarn.lock), compared case-insensitively; every files row's path is matched by some definitions.ownedPattern entry, where * matches any characters within one segment and a segment that is exactly ** matches any number of whole segments; a files row has mode 120000 exactly when its path is a discovery link, .claude/skills/clossys-<role> or .cursor/skills/clossys-<role> with <role> one segment; and the <role> of every path under .agents/skills/clossys-<role>/ and of every discovery link is a lowercase id token. L6: every keys row's pointer is /<placement>/<name with ~ written ~0 and / written ~1> of exactly one packages row, and its value is that row's version. L7: no planItem and no package name appears twice across packages and deferred together. L8: files are sorted by path, keys by file then pointer, entries by file, then key, then value, packages by planItem and deferred by planItem, strings comparing by UTF-16 code units, with no entry repeated; files paths compare case-insensitively for repetition. L9: every rootEntries row names the same file, the one Controller profile the flow edits, and each value is one of the root names an owned pattern can introduce (the first segment of a definitions.ownedPattern entry other than **), well within Controller's 255 UTF-16 code units. L10: every packages and deferred row's planItem is exactly repository.id, a colon and the row's name, in the same letter case. SUCCESSION, checked by comparing a pull request's head ledger with its base's ledger (the base's is absent before generation 1), each read from its bytes: each must be valid under this contract, and its bytes must be exactly the bytes RENDER gives it, so a repeated key, a byte order mark or any other spelling of a ledger is refused (rule bytes), never read as an unchanged ledger. S1: when head's bytes equal base's, the pull request makes no ledger change, and the rules below do not apply. S2: otherwise head.repository equals base.repository, head.generation is base.generation plus 1 (1 when the base has none), and head.history without its last entry equals base.history. S3: when head's last entry is admitted, the base has a ledger and its last history entry is the one setupChangeSet names; head.deferred is empty; head.entries equal base's, and head.files equal base's or add exactly one row and drop none, the row whose files[].path is the canonical Launcher guide artifact path (AGENTS_GUIDE_PATH), with mode 100644, after sha256:6f3d39117a95abeb969656c3e11eea52ad27bdafd2341b98ead38b67944e7a53 (the digest of the Launcher guide's bytes, so a row naming any other bytes is refused) and head's last changeSet, where the base holds no row at that path in any letter case; every base keys row and every base packages row is in head unchanged; and the other packages rows are exactly base.deferred's rows, each with the same planItem, act, name, version, integrity and placement and head's last changeSet; and the other keys rows each name one of those packages, with head's last changeSet. So an admitted generation installs exactly the packages the approved setup set deferred and changes no other row, apart from that one files row. An approved generation is checked by S2 alone, and it proves nothing: a reader without the hub cannot authenticate the approval it names, so for that reader an approved head generation is an unauthenticated claim of approval, never an admission and never a pass. A pull request could relabel an admitted generation approved to escape S3; a reader that must decide without the hub refuses such a head on an apply pull request, or treats the pull request as one that needs the client's own review. The result of a succession check therefore says which of three it found: no new generation, an admitted generation that S3 proved, or a claimed approval. Existing-declaration adoption: optional proof rows are covered by setup change-set approval, rendered with that setup digest into the protected ledger, and preserved exactly by the admitted apply generation. Only same-name, same-placement root declarations are eligible. Setup consumes pin-starter; install rows bind deferred identities, which the next admitted apply consumes. Prior literal declaration and resolved registry identity must be independently checked against protected source data, and desired identity against head data during setup and its immediate first apply. After a prior apply generation, retained consent is archival evidence of the original approved setup, authenticated by that held setup change set; it does not require a later current package identity to equal the historical desired identity. Metadata does not prove installed bytes. All default refusal paths remain.",
2341
2474
  "type": "object",
2342
2475
  "additionalProperties": false,
2343
2476
  "required": [
@@ -2422,6 +2555,14 @@ export const PLAN_CONTRACTS = {
2422
2555
  "items": {
2423
2556
  "$ref": "#/definitions/deferredRow"
2424
2557
  }
2558
+ },
2559
+ "existingDeclarationAdoptions": {
2560
+ "type": "array",
2561
+ "minItems": 1,
2562
+ "items": {
2563
+ "$ref": "#/definitions/existingDeclarationAdoption"
2564
+ },
2565
+ "description": "Explicit root declaration consent covered by the approved setup. Resolution evidence is metadata, not installed-byte proof. Omission preserves legacy behavior."
2425
2566
  }
2426
2567
  },
2427
2568
  "definitions": {
@@ -2833,6 +2974,126 @@ export const PLAN_CONTRACTS = {
2833
2974
  "description": "The setup set that deferred it (code rule L4)."
2834
2975
  }
2835
2976
  }
2977
+ },
2978
+ "adoptionResolution": {
2979
+ "type": "object",
2980
+ "additionalProperties": false,
2981
+ "required": [
2982
+ "name",
2983
+ "version",
2984
+ "integrity"
2985
+ ],
2986
+ "properties": {
2987
+ "name": {
2988
+ "type": "string",
2989
+ "pattern": "^@clossys/[a-z0-9-]+$"
2990
+ },
2991
+ "version": {
2992
+ "type": "string",
2993
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
2994
+ },
2995
+ "integrity": {
2996
+ "type": "string",
2997
+ "pattern": "^sha512-[A-Za-z0-9+/]{86}==$"
2998
+ }
2999
+ }
3000
+ },
3001
+ "adoptionDesired": {
3002
+ "type": "object",
3003
+ "additionalProperties": false,
3004
+ "required": [
3005
+ "name",
3006
+ "version",
3007
+ "integrity",
3008
+ "planItem",
3009
+ "act",
3010
+ "placement"
3011
+ ],
3012
+ "properties": {
3013
+ "name": {
3014
+ "type": "string",
3015
+ "pattern": "^@clossys/[a-z0-9-]+$"
3016
+ },
3017
+ "version": {
3018
+ "type": "string",
3019
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
3020
+ },
3021
+ "integrity": {
3022
+ "type": "string",
3023
+ "pattern": "^sha512-[A-Za-z0-9+/]{86}==$"
3024
+ },
3025
+ "planItem": {
3026
+ "type": "string"
3027
+ },
3028
+ "act": {
3029
+ "enum": [
3030
+ "install",
3031
+ "pin-starter"
3032
+ ]
3033
+ },
3034
+ "placement": {
3035
+ "enum": [
3036
+ "dependencies",
3037
+ "devDependencies"
3038
+ ]
3039
+ }
3040
+ }
3041
+ },
3042
+ "existingDeclarationAdoption": {
3043
+ "type": "object",
3044
+ "additionalProperties": false,
3045
+ "required": [
3046
+ "file",
3047
+ "placement",
3048
+ "name",
3049
+ "beforeVersion",
3050
+ "beforeResolved",
3051
+ "desired",
3052
+ "observedBaseCommit",
3053
+ "consent",
3054
+ "changeSet",
3055
+ "desiredSnapshotDigest"
3056
+ ],
3057
+ "properties": {
3058
+ "file": {
3059
+ "const": "package.json"
3060
+ },
3061
+ "placement": {
3062
+ "enum": [
3063
+ "dependencies",
3064
+ "devDependencies"
3065
+ ]
3066
+ },
3067
+ "name": {
3068
+ "type": "string",
3069
+ "pattern": "^@clossys/[a-z0-9-]+$"
3070
+ },
3071
+ "beforeVersion": {
3072
+ "description": "Stable exact, caret or tilde registry declaration; beforeResolved.version must satisfy this bounded declaration.",
3073
+ "type": "string",
3074
+ "pattern": "^[~^]?(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$"
3075
+ },
3076
+ "beforeResolved": {
3077
+ "$ref": "#/definitions/adoptionResolution"
3078
+ },
3079
+ "desired": {
3080
+ "$ref": "#/definitions/adoptionDesired"
3081
+ },
3082
+ "observedBaseCommit": {
3083
+ "type": "string",
3084
+ "pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$"
3085
+ },
3086
+ "consent": {
3087
+ "const": "adopt-existing-declaration"
3088
+ },
3089
+ "changeSet": {
3090
+ "$ref": "#/definitions/sha256Digest"
3091
+ },
3092
+ "desiredSnapshotDigest": {
3093
+ "type": "string",
3094
+ "pattern": "^sha256:[0-9a-f]{64}$"
3095
+ }
3096
+ }
2836
3097
  }
2837
3098
  }
2838
3099
  },