@clossys/launcher 0.3.0 → 0.4.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.
- package/README.md +1328 -60
- package/contracts/conversation-contract.md +2 -1
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +453 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +799 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +157 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +381 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +738 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +2840 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +187 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +532 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +198 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +840 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +250 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +827 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +493 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +34 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +557 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +54 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +427 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -5
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +20 -16
- package/skill-catalogue/advisor/SKILL.md +100 -11
- package/skill-catalogue/architect/SKILL.md +2 -13
- package/skill-catalogue/bouncer/SKILL.md +2 -13
- package/skill-catalogue/builder/SKILL.md +2 -13
- package/skill-catalogue/butler/SKILL.md +2 -13
- package/skill-catalogue/controller/SKILL.md +2 -13
- package/skill-catalogue/customer/SKILL.md +4 -13
- package/skill-catalogue/designer/SKILL.md +9 -14
- package/skill-catalogue/giver/SKILL.md +2 -13
- package/skill-catalogue/influencer/SKILL.md +2 -13
- package/skill-catalogue/inspector/SKILL.md +2 -13
- package/skill-catalogue/integrator/SKILL.md +2 -13
- package/skill-catalogue/keeper/SKILL.md +2 -13
- package/skill-catalogue/launcher/SKILL.md +20 -16
- package/skill-catalogue/locksmith/SKILL.md +2 -13
- package/skill-catalogue/messenger/SKILL.md +2 -13
- package/skill-catalogue/observer/SKILL.md +2 -13
- package/skill-catalogue/publisher/SKILL.md +11 -17
- package/skill-catalogue/starter/SKILL.md +3 -13
- package/skill-catalogue/strategist/SKILL.md +14 -17
- package/skill-catalogue/writer/SKILL.md +7 -14
- package/src/admission-fixture.ts +572 -0
- package/src/admission.ts +816 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +164 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +937 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +2840 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +72 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +637 -0
- package/src/ledger-trust.ts +267 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +886 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +964 -0
- package/src/plan-command.ts +509 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +571 -0
- package/src/setup-templates.ts +471 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
- package/CHANGELOG.md +0 -131
package/README.md
CHANGED
|
@@ -48,26 +48,99 @@ listed is no longer composed (its source disappeared), so launcher removes
|
|
|
48
48
|
its composed output and host discovery links — and only that. It never
|
|
49
49
|
touches a skill it did not itself write.
|
|
50
50
|
|
|
51
|
+
The recorded digest is how launcher tells whether it still owns a composed
|
|
52
|
+
skill. Before rewriting or retiring one, it compares the file on disk
|
|
53
|
+
(`.agents/skills/clossys-<package>/SKILL.md`, and any real-directory copy
|
|
54
|
+
at a host discovery path) with the digest it recorded when it last wrote
|
|
55
|
+
that file:
|
|
56
|
+
|
|
57
|
+
| On disk | Rewrite (skill still composed) | Retire (skill no longer composed) |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| Matches the recorded digest | Rewritten | Removed, with its discovery links |
|
|
60
|
+
| Edited since launcher wrote it | Left as is and reported | Left as is, with its discovery links, and reported |
|
|
61
|
+
| Missing | Recreated | Retirement completes (discovery links removed) |
|
|
62
|
+
| 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 |
|
|
63
|
+
|
|
64
|
+
A `SKILL.md` that exists but cannot be read (a permissions error, or a
|
|
65
|
+
directory in its place) is treated like an edited one: left as is and
|
|
66
|
+
reported. A retiring skill directory that holds files other than `SKILL.md`
|
|
67
|
+
is also left as is and reported; a macOS `.DS_Store` file is ignored for
|
|
68
|
+
this check. Each skill left as is appears in the health report as a
|
|
69
|
+
`skill preserved` line naming the file or directory that failed the check,
|
|
70
|
+
and in the report JSON under
|
|
71
|
+
`skillComposition.preserved`; it marks the report degraded, and it is
|
|
72
|
+
reported again on every run until resolved. Launcher recreates a missing
|
|
73
|
+
composed skill because `.agents/skills` is launcher-generated output, so
|
|
74
|
+
recreating it loses nothing a client wrote. That is also how to take
|
|
75
|
+
launcher's version of a skill you edited: move your copy aside, delete the
|
|
76
|
+
directory the `skill preserved` line names (usually
|
|
77
|
+
`.agents/skills/clossys-<package>/`), and run launcher again. To keep your
|
|
78
|
+
edit instead, leave the file as it is.
|
|
79
|
+
|
|
51
80
|
## Health report and staleness
|
|
52
81
|
|
|
53
82
|
After create, resume, or appoint — and on every resume — the command prints
|
|
54
83
|
a read-only health report. It scans all four dependency buckets
|
|
55
84
|
(`dependencies`, `devDependencies`, `optionalDependencies`,
|
|
56
|
-
`peerDependencies`) for the
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
`peerDependencies`) for the two hub engine pins, `@clossys/advisor` and
|
|
86
|
+
`@clossys/integrator` (an `advisor pin` and an `integrator pin` line), and
|
|
87
|
+
for extra `@clossys/*` names.
|
|
88
|
+
When an engine's live registry version is known, each of its pins is graded
|
|
89
|
+
against it: a pin older than live is a `stale pin` finding, named by package,
|
|
90
|
+
and marks the report **degraded**. The report is also degraded when either
|
|
91
|
+
engine is missing, dual-pinned, or present in any bucket other than
|
|
92
|
+
`devDependencies`, when a composed skill in the hub was left as is because a
|
|
93
|
+
client edited it (the `skill preserved` lines above), when the hub's stored
|
|
94
|
+
inventory fails its contract, and when the hub's lockfile does not resolve the
|
|
95
|
+
engine pins yet (an `install needed (engine-pins-changed-install-needed)` line;
|
|
96
|
+
see "Engine pins and the lockfile" below). Per-package
|
|
63
97
|
skill sources missing from the catalogue are noted but do not by themselves mark
|
|
64
|
-
degraded.
|
|
98
|
+
degraded. Each inventoried repository other than the hub gets a `sibling` line
|
|
99
|
+
(and an entry in `skillComposition.siblings`) saying what the run found for it:
|
|
100
|
+
a checkout beside the hub, one not cloned yet, another account's repository,
|
|
101
|
+
the Foundry supplier tree, a folder that is not a git checkout, a checkout git
|
|
102
|
+
refuses to read (dubious ownership), or a checkout whose git origin does not
|
|
103
|
+
match. The line names that repository by its position in the stored
|
|
104
|
+
inventory's `repositories` array (`repositories[<i>] in the stored
|
|
105
|
+
inventory`), never by its id (#1179), because the whole health report is also
|
|
106
|
+
JSON-dumped into the apply message's `health:` line. For a checkout beside the
|
|
107
|
+
hub or one not cloned yet, the line says a hub run writes nothing there, and
|
|
108
|
+
that once the repository is staffed in an approved plan, `@clossys-advisor`
|
|
109
|
+
and the voices of the roles staffed there arrive with that plan's setup pull
|
|
110
|
+
request. A sibling line never marks the report
|
|
111
|
+
degraded, and a sibling's working tree, output an earlier release wrote into it,
|
|
112
|
+
or its old pins do not change the hub run's result. Exit stays 0 on resume
|
|
65
113
|
(the report is advisory); adopt prints the same report and an unparseable
|
|
66
114
|
pin-versus-live comparison is noted as indeterminate rather than stale.
|
|
67
115
|
`checkInventoryEntries()` additionally validates hub inventory ids read-only,
|
|
68
116
|
marking ids whose repository no longer resolves (skipped with a note when
|
|
69
117
|
`gh` is unavailable).
|
|
70
118
|
|
|
119
|
+
### Engine pins and the lockfile
|
|
120
|
+
|
|
121
|
+
For engine pins, resume and appoint change only the hub's `package.json`, never its lockfile,
|
|
122
|
+
and install nothing. A run that changes an engine pin says exactly what it
|
|
123
|
+
changed (`engine pins changed in package.json: @clossys/advisor 0.2.6 -> 0.5.0;
|
|
124
|
+
@clossys/integrator added at 0.8.2`, and `health.enginePins` in the JSON) and
|
|
125
|
+
gives one next step: run the hub's package manager install (`npm install`,
|
|
126
|
+
`pnpm install`, `yarn install` or `bun install`, by the lockfile present), then
|
|
127
|
+
commit `package.json` together with its lockfile. Until then a frozen install
|
|
128
|
+
(`npm ci`, `pnpm install --frozen-lockfile`, `yarn install --immutable`) refuses
|
|
129
|
+
the hub when a pin's version changed or an engine was added; when a pin only
|
|
130
|
+
moved between dependency buckets, or a range became the exact version already
|
|
131
|
+
locked, whether it refuses depends on the package manager (pnpm's frozen
|
|
132
|
+
install does), so such a change is reported the same way. While a lockfile is present that does not resolve the pins yet, the
|
|
133
|
+
report is degraded with an `engine-pins-changed-install-needed` finding
|
|
134
|
+
(`health.installNeeded`): an npm lockfile (`npm-shrinkwrap.json`, which npm
|
|
135
|
+
prefers when both exist, else `package-lock.json`) is read on every run, and
|
|
136
|
+
each engine pinned to a plain version must resolve to that same version in it,
|
|
137
|
+
compared as versions (a `v0.6.0` pin matches a locked `0.6.0`); any other lockfile is not read, so
|
|
138
|
+
it counts as unresolved for the engines the run changed. Resume and appoint
|
|
139
|
+
only raise a pin: one older than live, or not a plain version, becomes the live version,
|
|
140
|
+
and one newer than live is kept. Resume rewrites `package.json` as
|
|
141
|
+
2-space-indented JSON with a final LF, only when a pin changes, and never
|
|
142
|
+
changes its `name`.
|
|
143
|
+
|
|
71
144
|
|
|
72
145
|
## Install
|
|
73
146
|
|
|
@@ -101,31 +174,41 @@ npm install --save-dev --save-exact @clossys/launcher@0.2.0
|
|
|
101
174
|
## Talking to the team
|
|
102
175
|
|
|
103
176
|
First contact is `npx @clossys/launcher` (empty directory or the checkout you
|
|
104
|
-
appoint as the hub). After apply, the launcher composes the
|
|
105
|
-
`@clossys-<package>` voices into the hub
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
appoint as the hub). After apply, the launcher composes the
|
|
178
|
+
`@clossys-<package>` voices into the hub (`.agents/skills/clossys-<package>/`
|
|
179
|
+
plus host discovery links). A launcher run writes no skills, skills manifest,
|
|
180
|
+
discovery links, `AGENTS.md` or `CLAUDE.md` into an inventoried product
|
|
181
|
+
repository, and changes nothing in its checkout beside the hub
|
|
182
|
+
(`--clone-missing`, below, only clones a missing one). Once a product
|
|
183
|
+
repository is staffed in an approved plan, it receives those files with that
|
|
184
|
+
plan's setup pull request; an inventoried repository that is not staffed does
|
|
185
|
+
not receive them. Composition is per checkout, not machine-wide, and is not a
|
|
186
|
+
catalogue dump into `package.json`.
|
|
109
187
|
|
|
110
188
|
Voices are how you talk in a coding agent; they are not engagement engines. The
|
|
111
189
|
`@clossys/advisor` npm package is the engine that grades evidence;
|
|
112
190
|
`@clossys-advisor` in chat is its hiring and compatibility voice. Use
|
|
113
|
-
`@clossys-advisor` and `@clossys-<package>` in the hub
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
191
|
+
`@clossys-advisor` and `@clossys-<package>` in the hub, where the whole team
|
|
192
|
+
is composed. A product repository staffed in an approved plan gets
|
|
193
|
+
`@clossys-advisor` and the voices of the roles staffed there, once that plan's
|
|
194
|
+
setup pull request has merged; a role not staffed there is expected to be
|
|
195
|
+
absent. Each voice can talk even when
|
|
196
|
+
that npm package is not pinned in that repo. When composing into the hub, the
|
|
197
|
+
launcher reads each skill body from the hub's installed
|
|
198
|
+
`@clossys/<package>/skill/SKILL.md` when present, then from the packed catalogue
|
|
199
|
+
or a sibling monorepo source. Launcher health
|
|
118
200
|
notes missing catalogue sources but apply continues.
|
|
119
201
|
|
|
120
202
|
Run `npx @clossys/launcher` again from the hub for a health report and to
|
|
121
|
-
refresh
|
|
203
|
+
refresh the voices composed in the hub. By default it does
|
|
122
204
|
not `gh repo clone` missing inventory entries -- that is not how you talk to
|
|
123
205
|
the team. `launcher --clone-missing` is the one explicit, approved
|
|
124
206
|
exception (#1179): on resume only, it clones every inventoried repository
|
|
125
207
|
not yet sitting beside the hub, using `cloneMissingInventoryRepositories()`,
|
|
126
208
|
and only those -- an id skipped for any other reason (wrong account, the
|
|
127
209
|
Foundry supplier tree, a mismatched git origin) is left exactly as skipped,
|
|
128
|
-
never attempted.
|
|
210
|
+
never attempted. Cloning is not composing: a repository cloned this way
|
|
211
|
+
receives its team only once it is staffed in an approved plan, like any other.
|
|
129
212
|
|
|
130
213
|
## How to run it
|
|
131
214
|
|
|
@@ -139,9 +222,9 @@ silent fallback.
|
|
|
139
222
|
|
|
140
223
|
| Current directory | What happens |
|
|
141
224
|
| --- | --- |
|
|
142
|
-
| Empty | Creates `{owner}/workspace` from the in-package skeleton (package name `@owner/workspace`), or
|
|
143
|
-
| Already a hub (generated marker; packed template `skeleton/clossys/.state/workspace.json`) | Resumes. No new repository. `--inventory` here is refused
|
|
144
|
-
| 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` in `devDependencies`, relocating
|
|
225
|
+
| 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. |
|
|
226
|
+
| 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). |
|
|
227
|
+
| 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. |
|
|
145
228
|
|
|
146
229
|
It does not have to be a brand-new exclusive repository, and it does not
|
|
147
230
|
have to already match a Foundry layout. Informal "workspace-looking" trees
|
|
@@ -158,10 +241,91 @@ yes in chat is permission for that one step only; it is not a lasting
|
|
|
158
241
|
grant and it does not write git unless a file is saved later. The same
|
|
159
242
|
command resumes later.
|
|
160
243
|
|
|
244
|
+
## Choosing the hub's repositories
|
|
245
|
+
|
|
246
|
+
A founder never writes the inventory by hand (#1179). Advisor's repository
|
|
247
|
+
card (`@clossys/advisor`'s `repositoryChoiceCard()`, or its
|
|
248
|
+
`advisor-repository-card` bin before the hub exists) offers the
|
|
249
|
+
repositories the agent listed from GitHub for the founder's sign-in; the
|
|
250
|
+
founder chooses; and the agent passes the chosen ids to Launcher:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
launcher --repositories example-owner/example-app,example-owner/example-site
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`--repositories` takes one argument of comma-separated repository ids (an
|
|
257
|
+
id never contains a comma). It works when appointing a repository as the
|
|
258
|
+
hub and on an existing hub checkout; in an empty directory it is refused,
|
|
259
|
+
because there is no hub to write into yet. Launcher writes the chosen ids to
|
|
260
|
+
`clossys/.state/inventory.json` (a generated hub path, not shipped in this package)
|
|
261
|
+
-- the one inventory location it reads on
|
|
262
|
+
every later run -- and, on resume, writes it before the health report is
|
|
263
|
+
built, so the same run lists the repositories just chosen as `sibling` lines.
|
|
264
|
+
|
|
265
|
+
- The ids, and the document built from them, are checked against
|
|
266
|
+
`docs/contracts/repository-inventory.json` (in the public repository;
|
|
267
|
+
that exact path does not ship in this package, but this package's build
|
|
268
|
+
packs and ships its own copy of the contract) through the same shared
|
|
269
|
+
contract checker every read of the inventory uses, and the document is
|
|
270
|
+
checked again by that reader before it is written. What Launcher writes is
|
|
271
|
+
what Launcher reads back.
|
|
272
|
+
- A malformed choice -- an empty id, an id that is not a bare name or
|
|
273
|
+
`owner/name`, or two ids naming the same repository -- is refused by
|
|
274
|
+
position (`repositories[1].id ...`), never echoed, and nothing is written.
|
|
275
|
+
- Launcher decides "is this the same repository" one way everywhere: a
|
|
276
|
+
bare id names a repository of the hub's own owner, and letter case is
|
|
277
|
+
ignored, so `app`, `App` and `<owner>/app` are one repository. Owners are
|
|
278
|
+
compared the same way. A stored inventory, or a choice, that lists one
|
|
279
|
+
repository twice this way is refused, naming the two positions; it is
|
|
280
|
+
never merged. The hub itself is recognised by its origin's `owner/name`
|
|
281
|
+
(or, without a github.com origin, the repository its marker records),
|
|
282
|
+
never by its folder path, so an inventory that names the hub in another
|
|
283
|
+
letter case never lists it as its own sibling.
|
|
284
|
+
- An inventory that already lists exactly the chosen repositories, in any
|
|
285
|
+
order or letter case, or with a bare id for the hub's own owner, is left
|
|
286
|
+
as it is.
|
|
287
|
+
- An inventory that lists a different set is never merged into or
|
|
288
|
+
overwritten silently: the run is refused, stating how many repositories
|
|
289
|
+
each side has and which positions would be added and removed --
|
|
290
|
+
`--repositories[<i>]` for an added id's position in the `--repositories`
|
|
291
|
+
argument, `repositories[<i>] in the stored inventory` for a removed id's
|
|
292
|
+
position in the file -- never the ids themselves, because both the stored
|
|
293
|
+
inventory file and `--repositories` are input an agent may relay
|
|
294
|
+
verbatim, and a repository id is exactly the kind of short string a
|
|
295
|
+
hostile inventory entry could shape as prompt-injection text. Run again
|
|
296
|
+
with `--replace-inventory` to approve the replacement; a repository that
|
|
297
|
+
stays keeps its existing entry, `packages` included. An agent acting on
|
|
298
|
+
the refusal looks each reported position up in its own copy of the
|
|
299
|
+
stored inventory file or its own `--repositories` argument to learn which
|
|
300
|
+
repository it names, and tells the founder that name -- never the
|
|
301
|
+
position string itself, and never text read back out of the inventory
|
|
302
|
+
file or the argument without that lookup. That replacement run's own
|
|
303
|
+
success line reports the same removed positions again, distinctly
|
|
304
|
+
labeled `repositories[<i>] in the replaced inventory`: by the time that
|
|
305
|
+
line prints, `clossys/.state/inventory.json` is already the new file, so
|
|
306
|
+
reusing "in the stored inventory" there would point a reader at the
|
|
307
|
+
wrong document. The positions still index into the file as it stood
|
|
308
|
+
before this run -- the same one the refusal step already named -- so an
|
|
309
|
+
agent that already looked a position up there does not need to look it
|
|
310
|
+
up again. The same position-only rule, and the same "in the stored
|
|
311
|
+
inventory" wording, applies to every other message this command prints
|
|
312
|
+
that names a repository from the file currently on disk -- each
|
|
313
|
+
`sibling (...)` line and `launcher --clone-missing`'s output. (Since
|
|
314
|
+
#1511 the `skill roster written` health-report line names only the
|
|
315
|
+
hub's own id, never a stored-inventory position, so it is no longer on
|
|
316
|
+
this list.)
|
|
317
|
+
- An inventory that fails its contract is likewise replaced only with
|
|
318
|
+
`--replace-inventory`.
|
|
319
|
+
- `--repositories` and `--inventory` each supply the whole inventory, so
|
|
320
|
+
they are refused together, and `--replace-inventory` without
|
|
321
|
+
`--repositories` is refused.
|
|
322
|
+
|
|
161
323
|
## CLI
|
|
162
324
|
|
|
163
325
|
```bash
|
|
164
326
|
launcher
|
|
327
|
+
launcher --repositories example-owner/example-app,example-owner/example-site
|
|
328
|
+
launcher --repositories example-owner/example-app --replace-inventory
|
|
165
329
|
launcher --inventory path/to/inventory.json
|
|
166
330
|
launcher --clone-missing
|
|
167
331
|
launcher --help
|
|
@@ -169,6 +333,12 @@ launcher-check --help
|
|
|
169
333
|
launcher-check --input observation.json
|
|
170
334
|
launcher-doctor
|
|
171
335
|
launcher-apply-plan --plan plan.json --brief brief.json --repo ./product-checkout
|
|
336
|
+
launcher-apply-plan plan
|
|
337
|
+
launcher-apply-plan materialize --repo ./site-checkout
|
|
338
|
+
launcher-apply-plan verify --repo ./site-checkout
|
|
339
|
+
launcher-apply-plan status --repo ./site-checkout
|
|
340
|
+
launcher-apply-plan body --repo "<id>" --task-record 12
|
|
341
|
+
launcher-apply-plan snapshot --request package-request.json
|
|
172
342
|
```
|
|
173
343
|
|
|
174
344
|
Exit codes preserve the ternary:
|
|
@@ -176,8 +346,10 @@ Exit codes preserve the ternary:
|
|
|
176
346
|
| Exit | State | Meaning |
|
|
177
347
|
| --- | --- | --- |
|
|
178
348
|
| `0` | `satisfied` | Created, resumed, or appointed the hub. The message includes a read-only health report. |
|
|
179
|
-
| `1` | `violated` | Known refusal: not GitHub, not empty, missing appoint inventory, the supplier tree, uncommitted changes in the appoint tree, or a `CLOSSYS_OWNER` that disagrees with the origin owner. |
|
|
180
|
-
| `2` | `indeterminate` | Missing `gh`,
|
|
349
|
+
| `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. |
|
|
350
|
+
| `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. |
|
|
351
|
+
|
|
352
|
+
`launcher-apply-plan` has its own exit codes, described in [Applying an approved plan](#applying-an-approved-plan) and [Taking the registry snapshot](#taking-the-registry-snapshot).
|
|
181
353
|
|
|
182
354
|
`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.
|
|
183
355
|
|
|
@@ -185,41 +357,54 @@ Exit codes preserve the ternary:
|
|
|
185
357
|
|
|
186
358
|
| Export | Description |
|
|
187
359
|
| --- | --- |
|
|
188
|
-
| `planWorkspace()` | Decides create, resume, or adopt from a cwd observation. Optional `{ inventoryPath }`
|
|
189
|
-
| `applyWorkspacePlan()` | Copies the in-package skeleton or hub marker through a host port and returns a `WorkspaceApplyResult` with health. Composes the
|
|
190
|
-
| `observeWorkspace()` | Reads `gh`, git remotes, cwd, inventory classification, hub-state migration status, and the public Advisor version. |
|
|
191
|
-
| `readInventoryRepositories()` | Reads repository ids from
|
|
360
|
+
| `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. |
|
|
361
|
+
| `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. |
|
|
362
|
+
| `observeWorkspace()` | Reads `gh`, git remotes, cwd, inventory classification, hub-state migration status, and the public Advisor and Integrator versions (`npm view <package> version`). |
|
|
363
|
+
| `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. |
|
|
192
364
|
| `readLiveLauncherVersion()` | Reads the public `@clossys/launcher` registry version, used only to grade catalogue-sourced skill staleness. |
|
|
193
365
|
| `launcherPackageRootFromModule()` | Resolves this package's root from `import.meta.url` so apply can find the packed skill catalogue and contract. |
|
|
194
366
|
| `parseGitHubRemote()` | Parses a github.com remote and rejects any other host. |
|
|
195
367
|
| `isHubDocument()` | Type guard for the generated hub marker (packed template: `skeleton/clossys/.state/workspace.json`). |
|
|
196
|
-
| `inspectInventory()` | Classifies inventory
|
|
197
|
-
| `
|
|
368
|
+
| `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()`). |
|
|
369
|
+
| `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). |
|
|
370
|
+
| `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. |
|
|
198
371
|
| `formatHubHealth()` | Human lines plus a `health:` JSON line for the same report. |
|
|
199
372
|
| `hasAdvisorPin()` | True when a manifest already pins Advisor in any dependency bucket. |
|
|
200
373
|
| `checkInventoryEntries()` | Read-only inventory id validation through `gh repo view` (batched; skips with a note when `gh` is unavailable). |
|
|
201
|
-
| `DEFAULT_REPOSITORY_NAME` | Default
|
|
374
|
+
| `DEFAULT_REPOSITORY_NAME` | Default hub repository name (`workspace`): the repository an empty-directory run creates, or clones when it already exists. |
|
|
202
375
|
| `CLOSSYS_DIR_REL` | Relative path of the one visible per-repository Clossys folder (`clossys`). |
|
|
203
376
|
| `STATE_DIR_REL` | Relative path of the machine-state folder (`clossys/.state`). |
|
|
204
377
|
| `WORKSPACE_MARKER_REL` | Relative path of the hub marker. |
|
|
205
378
|
| `WORKSPACE_INVENTORY_REL` | Relative path of the hub inventory. |
|
|
206
379
|
| `CLOSSYS_README_REL` | Relative path of the generated index README at the root of `clossys/`. |
|
|
207
380
|
| `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. |
|
|
208
|
-
| `CommandResult` / `CwdObservation` / `DependencyBucket` / `HubDocument` / `HubHealthReport` / `HubMigrationState` / `InventoryObservation` / `InventoryValidationEntry` / `InventoryValidationReport` / `PinFinding` / `PinGrade` / `SkillManifestDocument` / `SkillManifestEntry` / `SkillsManifestSummary` / `ApplyWorkspaceOptions` / `WorkspaceApplyResult` / `WorkspaceDecision` / `WorkspaceHost` / `WorkspaceObservation` / `WorkspacePlan` / `WorkspaceRefusal` / `WorkspaceState` | Typed host, observation, plan, health, and outcome contracts. |
|
|
209
|
-
| `cloneMissingInventoryRepositories()` | Explicit, approved action (#1179): clones every inventoried repository
|
|
381
|
+
| `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()`). |
|
|
382
|
+
| `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[]`. |
|
|
210
383
|
| `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`. |
|
|
211
384
|
| `renderDoctorReport()` | Renders a `DoctorReport` one step at a time, the way `launcher-doctor` prints it. |
|
|
212
385
|
| `checkCloudSessionBootstrap()` | Read-only: the three product-repository-layout.json cloud-session-bootstrap checks against a directory. Returns a `CloudBootstrapReport`. |
|
|
213
|
-
| `reportInventoryDrift()` | Compares a declared external inventory against the launcher-written one; reports external-only, launcher-only, and agreeing
|
|
386
|
+
| `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`. |
|
|
214
387
|
| `detectLinkedHosts()` | Read-only: which of `claude-code`, `cursor`, `codex` can currently discover skills in a directory. |
|
|
215
388
|
| `serializeHostRecord()` / `parseHostRecord()` | Round-trip `clossys/.state/hosts.json` (`HOSTS_REL`). |
|
|
216
389
|
| `parsePreferences()` | Reads `clossys/preferences.json`'s budget stance; defaults to `"balanced"` on absence or malformed input. |
|
|
217
390
|
| `readHostModelProfile()` | Reads a packed `model-profiles/<host>.json`; returns `undefined`, never throws, on a missing or malformed file. |
|
|
218
391
|
| `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. |
|
|
219
|
-
| `validateAdvisorPlan()` / `validateEngagementBrief()` |
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
392
|
+
| `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. |
|
|
393
|
+
| `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()`. |
|
|
394
|
+
| `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. |
|
|
395
|
+
| `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. |
|
|
396
|
+
| `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. |
|
|
397
|
+
| `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. |
|
|
398
|
+
| `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. |
|
|
399
|
+
| `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`. |
|
|
400
|
+
| `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). |
|
|
401
|
+
| `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`. |
|
|
402
|
+
| `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. |
|
|
403
|
+
| `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. |
|
|
404
|
+
| `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. |
|
|
405
|
+
| `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()`. |
|
|
406
|
+
| `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. |
|
|
407
|
+
| `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. |
|
|
223
408
|
|
|
224
409
|
## Doctor
|
|
225
410
|
|
|
@@ -247,7 +432,67 @@ session needs: a resolvable `package.json` plus `package-lock.json` pair,
|
|
|
247
432
|
an `AGENTS.md` that mentions `clossys/`, and a hub marker at the same
|
|
248
433
|
relative path as the packed template `skeleton/clossys/.state/workspace.json`.
|
|
249
434
|
It never runs `npm ci` itself and never mutates anything; it only reports
|
|
250
|
-
which of the three is missing.
|
|
435
|
+
which of the three is missing. A launcher run in the hub writes nothing
|
|
436
|
+
into a product repository, so until the repository is staffed in an approved
|
|
437
|
+
plan and that plan's setup pull request merges, an unsatisfied
|
|
438
|
+
`agents-pointer` check is the expected state; its note says so.
|
|
439
|
+
|
|
440
|
+
### Setup templates
|
|
441
|
+
|
|
442
|
+
`renderSetupTemplate()` and the renderers beside it are pure functions that
|
|
443
|
+
return the exact bytes of the files a setup change writes. Nothing writes those
|
|
444
|
+
bytes yet; a later step plans them into a change set. The renderers behind it
|
|
445
|
+
are exported too: `renderStarterRequest()`, `renderAdoptionDecisionWorkflow()`,
|
|
446
|
+
`renderProductCiWorkflow()`, `renderAdoptionEvidenceWorkflow()`,
|
|
447
|
+
`renderSnapshotCollector()`, `renderPathScopeWorkflow()` and
|
|
448
|
+
`renderPathScopeScript()`, the standalone script the path-scope workflow embeds.
|
|
449
|
+
A `TemplateResult` is either `{ ok: true, files }`, a list of `TemplateFile`
|
|
450
|
+
entries (`path` and `bytes`), or `{ ok: false, refusal }` with a
|
|
451
|
+
`TemplateRefusal`; the package manager is a `SetupPackageManager` and the Starter
|
|
452
|
+
pin a `StarterPinInput`.
|
|
453
|
+
|
|
454
|
+
There are four acts, each with a fixed file list that `renderSetupTemplate()`
|
|
455
|
+
returns in this order:
|
|
456
|
+
|
|
457
|
+
- `add-caller-workflow` takes `{ packageManager }` and returns
|
|
458
|
+
`.github/workflows/clossys-adoption-evidence.yml`,
|
|
459
|
+
`.github/workflows/clossys-adoption-decision.yml` and
|
|
460
|
+
`.github/scripts/clossys-collect-adoption-snapshot.mjs`.
|
|
461
|
+
- `write-starter-request` takes a `StarterRequestInput` and returns
|
|
462
|
+
`.starter/request.json`.
|
|
463
|
+
- `add-ci-template` takes no input and returns `.github/workflows/clossys-ci.yml`.
|
|
464
|
+
- `add-path-scope-job` takes no input and returns
|
|
465
|
+
`.github/workflows/clossys-path-scope.yml`.
|
|
466
|
+
|
|
467
|
+
The request is in the admission phase and names the Starter as both its own
|
|
468
|
+
engine and its target, and it names both evidence paths, the assessment file
|
|
469
|
+
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.3.0`
|
|
470
|
+
(`STARTER_PIN_RANGE`); any other pin is refused as `starter-pin-unsupported`,
|
|
471
|
+
and a package manager other than npm or pnpm, Yarn included, is refused as
|
|
472
|
+
`package-manager-unsupported`. A refusal names a position such as
|
|
473
|
+
`starter.version` and never quotes the value it refused.
|
|
474
|
+
|
|
475
|
+
The decision workflow starts only on `workflow_run` completion of the evidence
|
|
476
|
+
workflow, and its job carries no condition, so it starts for every conclusion.
|
|
477
|
+
It checks out the protected pull request base, runs one fixed frozen install
|
|
478
|
+
(`npm ci --ignore-scripts` or `pnpm install --frozen-lockfile --ignore-scripts`),
|
|
479
|
+
and runs the installed Starter's `admit` command over a sparse checkout of the
|
|
480
|
+
`workflow_run` head that holds only `/clossys/.state/installed.json`. The
|
|
481
|
+
trusted decision job never reads or trusts the snapshot artifact the evidence
|
|
482
|
+
workflow uploads, because a pull request controls that workflow. The collector
|
|
483
|
+
script is still written because the contract's file set for the caller
|
|
484
|
+
workflows names it.
|
|
485
|
+
|
|
486
|
+
The path-scope job applies to pull requests whose head branch starts with
|
|
487
|
+
`clossys/apply-`, and fails when a changed path is outside the paths Clossys may
|
|
488
|
+
own (`OWNED_PATH_PATTERNS`) or, apart from the ledger, `package.json` and the
|
|
489
|
+
lockfiles, is not named by the pull request's own ledger. It runs in the pull
|
|
490
|
+
request's own context, so it catches an agent's mistakes, not a hostile author;
|
|
491
|
+
the admission job runs from the protected base.
|
|
492
|
+
|
|
493
|
+
A change to any of these workflows, or to `.starter/request.json`, is proved
|
|
494
|
+
only by the first pull request after it merges, because the decision runs from
|
|
495
|
+
the base: a one-merge lag.
|
|
251
496
|
|
|
252
497
|
## Inventory: adopting an existing source
|
|
253
498
|
|
|
@@ -277,8 +522,8 @@ presence of `.agents/skills` itself, since Codex reads repository skills
|
|
|
277
522
|
from that path directly and needs no separate discovery link (verified
|
|
278
523
|
against developers.openai.com/codex/skills, 2026-09-22). The snapshot is
|
|
279
524
|
written to `clossys/.state/hosts.json` (`HOSTS_REL`, via
|
|
280
|
-
`serializeHostRecord()` / `parseHostRecord()`) for the hub
|
|
281
|
-
|
|
525
|
+
`serializeHostRecord()` / `parseHostRecord()`) for the hub, the one checkout
|
|
526
|
+
a launcher run composes skills into -- so a consumer such as
|
|
282
527
|
Advisor's next-action phrasing can name the client's actual tool instead
|
|
283
528
|
of guessing.
|
|
284
529
|
|
|
@@ -302,21 +547,1039 @@ tier cannot be met.
|
|
|
302
547
|
`launcher-apply-plan --plan <plan.json> --brief <brief.json> --repo <dir>`
|
|
303
548
|
writes `clossys/brief.json` into a staffed repository once the plan is
|
|
304
549
|
approved (#1178). `validateAdvisorPlan()` and `validateEngagementBrief()`
|
|
305
|
-
check both files against the
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
`
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
550
|
+
check both files against the shared contracts in this repository's
|
|
551
|
+
`docs/contracts/` -- `advisor-plan.json`, `engagement-brief.json`, and
|
|
552
|
+
`engagement-context.json` for the brief's `context` snapshot (#1475). This
|
|
553
|
+
package's build packs those files, with a copy of the one contract checker
|
|
554
|
+
`@clossys/advisor` uses, so Launcher and Advisor accept exactly the same
|
|
555
|
+
plans and briefs while Launcher keeps no runtime dependency on Advisor.
|
|
556
|
+
Every object is closed: a field the contracts do not declare is refused,
|
|
557
|
+
and a known context value must be one of that field's fixed choice ids,
|
|
558
|
+
because the brief is committed in every staffed repository. A string or
|
|
559
|
+
key containing a lone surrogate is refused, so every plan that validates has
|
|
560
|
+
a digest. Every plan time must be a real calendar date or date-time in
|
|
561
|
+
ISO 8601 form, a date-time with `Z` or a `±hh:mm` offset, checked
|
|
562
|
+
field by field (so `2026-02-30` or `T24:30` is refused). A brief's
|
|
563
|
+
`problem`, `role`, `why` and `metric` must contain a non-whitespace
|
|
564
|
+
character, and an item of `inputsFrom`, `outputsTo`, `sequence` or
|
|
565
|
+
`deliverables` must not be empty. A refusal names each declared field at
|
|
566
|
+
fault and never echoes its value, and never names a key the contracts do
|
|
567
|
+
not declare: such a field is reported at the object that holds it, by its
|
|
568
|
+
1-based position there (`plan.mandate has a field the contract does not
|
|
569
|
+
declare (key 4 of this object), and unknown fields are refused`), counted
|
|
570
|
+
in the order the file wrote the keys when `launcher-apply-plan` reads it; a
|
|
571
|
+
value a caller passes to a validator directly is counted in JavaScript's
|
|
572
|
+
own key order, which lists array-index keys such as `"7"` first.
|
|
573
|
+
`launcher-apply-plan` reads both files as strict JSON: bytes that are not
|
|
574
|
+
valid UTF-8, a leading byte order mark, or an object that repeats a key at
|
|
575
|
+
any depth exit `2`, with a repeated key reported by its position in its
|
|
576
|
+
object and, below the top level, that object's position, never by name,
|
|
577
|
+
and a syntax error by position only, never quoting the file's text, so the
|
|
578
|
+
value validated is exactly the one a reader of the file sees. A position is
|
|
579
|
+
a 0-based index into the decoded text in UTF-16 code units (JavaScript's
|
|
580
|
+
string index): the byte offset for ASCII text, with a character outside
|
|
581
|
+
the Basic Multilingual Plane counting as two.
|
|
582
|
+
A plan may say which roles work in which repository (`staffing`, by
|
|
583
|
+
repository inventory id), which kits were recommended (`kits`), which exact
|
|
584
|
+
package acts are authorized (`packages`, each one exact version and one
|
|
585
|
+
`sha512-` integrity value) and where those versions were resolved from
|
|
586
|
+
(`resolution`) (#1178). Once the schema passes, the contract's code rules
|
|
587
|
+
run: no repository is staffed twice (ids compare case-insensitively),
|
|
588
|
+
every staffed role is one of the mandate's roles and every mandate role is
|
|
589
|
+
staffed somewhere unless it is a hub-only role, every
|
|
590
|
+
package act names a staffed repository spelled exactly the same, no
|
|
591
|
+
`planItem` repeats, no package appears twice in one repository,
|
|
592
|
+
`resolution` is present exactly when `packages` is, no kit id repeats, no
|
|
593
|
+
role repeats within one staffing entry, no role is named twice in
|
|
594
|
+
`mandate.roles`, a repository has at most one `pin-starter` act, always
|
|
595
|
+
placed in `devDependencies`, no hub-only role is staffed, and every
|
|
596
|
+
`planItem` is exactly its act's `repository`, a colon and its `name`, in the
|
|
597
|
+
same letter case (R12). The hub-only
|
|
598
|
+
roles, Advisor and Integrator, are read from the plan contract's
|
|
599
|
+
`definitions.hubOnlyRoles`, the same data Advisor reads. The rules read only a document's own fields,
|
|
600
|
+
as the schema does, so an inherited one is ignored. A brief's optional `staffedHere`
|
|
601
|
+
must name only the brief's own roles, each once. This package implements
|
|
602
|
+
those rules separately from Advisor, and both are tested against the same
|
|
603
|
+
corpus, `docs/contracts/advisor-plan-rules.fixture.json` (in the public
|
|
604
|
+
repository, not shipped in this package).
|
|
605
|
+
`approvedSubject()` says what an approval binds: the `subjectDigest` of the
|
|
606
|
+
plan's most recent decision (by timestamp, not array position) when it is
|
|
607
|
+
`"approved"`. It validates the plan itself first and returns null for one
|
|
608
|
+
that does not validate. An approval with no `subjectDigest` binds nothing,
|
|
609
|
+
and so does no decision at all, a decision time that does not parse, or a tie at
|
|
610
|
+
the latest instant between decisions that disagree or name different
|
|
611
|
+
subjects. It says what was approved, not that it matches: a caller must
|
|
612
|
+
recompute the digest of the change it holds and compare.
|
|
613
|
+
`isPlanApproved()` reads the same most recent decision and is true when it
|
|
614
|
+
is `"approved"`, with the same rules for ties and unreadable times, but it
|
|
615
|
+
binds no bytes: it ignores `subjectDigest`, so it is also true for an
|
|
616
|
+
approval that names no change. Anything that applies a plan must use
|
|
617
|
+
`approvedSubject()` instead.
|
|
618
|
+
`applyEngagementBrief()` and `launcher-apply-plan` are the brief-only path,
|
|
619
|
+
which predates that binding and is kept as it was, not extended: they
|
|
620
|
+
require `isPlanApproved()` and accept an approval with or without a
|
|
621
|
+
`subjectDigest`, checking no binding.
|
|
622
|
+
`applyEngagementBrief()` refuses, and writes nothing, unless all three
|
|
623
|
+
checks pass and the plan's digest is computed; only then does it write the brief byte-identically -- it never re-authors
|
|
624
|
+
its prose -- and reports `planDigest()` of the plan it applied, which the
|
|
625
|
+
CLI prints as `plan digest sha256:...`. That digest is defined once, in
|
|
626
|
+
`docs/contracts/advisor-plan-digest.md` (in the public repository, not shipped in this package);
|
|
627
|
+
this package and Advisor each
|
|
628
|
+
implement it and are tested against the same fixture corpus. On this
|
|
629
|
+
brief-only path, this package does not compute the brief's content -- it
|
|
630
|
+
writes the brief it is given, which `@clossys/advisor`'s
|
|
631
|
+
`toEngagementBrief()` builds, and applies no per-repository projection --
|
|
632
|
+
and it does not decide whether a plan should be approved (that is
|
|
633
|
+
Advisor's job); it only validates the two shapes and writes the one file.
|
|
634
|
+
The apply planner below is different: it projects each repository's brief
|
|
635
|
+
from the hub brief itself.
|
|
636
|
+
|
|
637
|
+
Launcher's packed skill carries the agent procedure that puts a stored change
|
|
638
|
+
set into a staffed repository, under "Apply an approved plan" (#1762). The
|
|
639
|
+
agent verifies, files the task-record issue, prints the pull request body with
|
|
640
|
+
`launcher-apply-plan body`, commits and pushes only the set's `clossys/apply-`
|
|
641
|
+
branch, opens the pull request, reads `launcher-apply-plan status`, and reports.
|
|
642
|
+
Launcher never pushes, opens a pull request, files an issue or merges, and the
|
|
643
|
+
procedure forbids merging and enabling auto-merge. `check-package-skills` pins
|
|
644
|
+
each of those rules in the packed skill text.
|
|
645
|
+
|
|
646
|
+
### Release-age exemption
|
|
647
|
+
|
|
648
|
+
`editReleaseAgeExemption()` computes the edit that lists the publishing
|
|
649
|
+
scope's `<scope>/*` entry as exempt from a package manager's release-age
|
|
650
|
+
delay (#1178). It is pure: it does no I/O. It takes a surface
|
|
651
|
+
(`pnpm-workspace` or `yarnrc`), the surface file's text or `null`, and, for
|
|
652
|
+
pnpm, the `.npmrc` text or `null`. It returns `edited` with the exact new
|
|
653
|
+
text, `unchanged` when the scope entry is already listed, or a refusal
|
|
654
|
+
(`ReleaseAgeEdit`, with `ReleaseAgeEditInput` and
|
|
655
|
+
`ReleaseAgeEditRefusalReason`).
|
|
656
|
+
|
|
657
|
+
The entry goes under `minimumReleaseAgeExclude` (pnpm, single-quoted) or
|
|
658
|
+
`npmPreapprovedPackages` (Yarn, double-quoted). A missing file becomes the
|
|
659
|
+
key alone, a top-level block sequence of scalars gets one new entry after
|
|
660
|
+
its last item, and a file without the key gets the key appended. Other
|
|
661
|
+
bytes, including comments and the final newline, are kept. The function
|
|
662
|
+
reads only the shapes it recognises: `release-age-surface-unparseable` covers
|
|
663
|
+
a flow sequence, an anchor, an alias, a tag, a comment inside the list,
|
|
664
|
+
several documents, a tab, a carriage return or byte order mark, a repeated
|
|
665
|
+
key, and a value that is not a block sequence of scalars.
|
|
666
|
+
|
|
667
|
+
For pnpm the `.npmrc` is read under a fixed grammar and refused otherwise.
|
|
668
|
+
Every line must be blank, a comment (first non-space character `#` or `;`),
|
|
669
|
+
or a plain `key=value` assignment, optionally spaced around the `=`, whose key
|
|
670
|
+
is only ASCII letters, digits and `@ : _ . / -`. An `.npmrc` containing any
|
|
671
|
+
line outside those shapes (a quoted or bracketed key, a comment or escape
|
|
672
|
+
inside the key, a tab, a section header, a key with no `=`) is refused as
|
|
673
|
+
`release-age-surface-unparseable`, because npm's ini reader could read such a
|
|
674
|
+
line as the exclusion setting. A plain key that is `userconfig`,
|
|
675
|
+
`globalconfig` or `prefix` (in any case, with `-` and `_` ignored) is refused
|
|
676
|
+
the same way, because it points npm at another config file or prefix
|
|
677
|
+
directory whose own exclusion list the editor cannot read. A plain key that is
|
|
678
|
+
`minimum-release-age-exclude` in any case, with `-` and `_` ignored (so the
|
|
679
|
+
camel-case spelling too), is refused as `release-age-surface-conflict`.
|
|
680
|
+
Refusing is the default: an unrelated `.npmrc` line the grammar does not list
|
|
681
|
+
also refuses the whole file, and the caller resolves the file by hand.
|
|
682
|
+
|
|
683
|
+
`verifyReleaseAgeExemption()` takes the surface, the text before, the text
|
|
684
|
+
after, and, for pnpm, the `.npmrc` text. It reports a `ReleaseAgeVerdict`, `{ verified: true, value }`
|
|
685
|
+
(its input is a `ReleaseAgeVerifyInput`), only when the two texts differ by that one added entry, read again with the
|
|
686
|
+
same rules; `value` is the `<scope>/*` string the installed-state ledger's
|
|
687
|
+
`entries` row holds. It returns `{ verified: false }` for an unchanged file
|
|
688
|
+
(`before` equal to `after`) and for any `.npmrc` that is a conflict or outside
|
|
689
|
+
the grammar above, so a wiring unit must not verify after an `unchanged` or
|
|
690
|
+
`refused` result. It says nothing about whether a given pnpm or Yarn
|
|
691
|
+
version honours the key; that is proved separately with pinned tools.
|
|
692
|
+
|
|
693
|
+
### Computing each repository's change
|
|
694
|
+
|
|
695
|
+
`planApplyBundle()` computes, for each repository a plan staffs, the change
|
|
696
|
+
set one pull request would make there, and a bundle that holds them (#1178).
|
|
697
|
+
It is pure: it takes the plan, the hub brief (`clossys/advisor/brief.json`),
|
|
698
|
+
what the caller observed on each repository's default branch (including the
|
|
699
|
+
exact bytes of its installed-state ledger and its composed-skill manifest),
|
|
700
|
+
the change sets the hub holds, the composed skill text for each role and for
|
|
701
|
+
Advisor, this package's version and the hub's Advisor and Integrator pins,
|
|
702
|
+
and it reads nothing itself. The shapes are the shared contracts
|
|
703
|
+
`docs/contracts/repository-change-set.json` and `apply-bundle.json`, packed
|
|
704
|
+
into this package, and every set and the bundle are validated against them,
|
|
705
|
+
code rules included, before they are returned. The digests are defined in
|
|
706
|
+
`docs/contracts/apply-change-set-digest.md`, with a corpus computed
|
|
707
|
+
independently of this package (both in the public repository, not shipped
|
|
708
|
+
in this package).
|
|
709
|
+
|
|
710
|
+
- Each set describes the repository's brief, projected from the hub brief with
|
|
711
|
+
`staffedHere` set to its roles and, unless the repository is private, the
|
|
712
|
+
brief contract's fixed placeholder in place of the client's problem. It
|
|
713
|
+
composes the Advisor voice together with each staffed role's voice (D33;
|
|
714
|
+
`staffedHere` still lists the staffed roles only), and holds each of those
|
|
715
|
+
skills, a discovery link to it under
|
|
716
|
+
`.claude/skills` and `.cursor/skills` (none under a root the default
|
|
717
|
+
branch has as a symbolic link), and `clossys/.state/skills.json` listing
|
|
718
|
+
the skills it writes. It carries every package act the plan names for
|
|
719
|
+
that repository, the hub's exact Integrator pin, and names the
|
|
720
|
+
installed-state ledger as a derived file. No act the plan authorizes is
|
|
721
|
+
dropped and no other act is added. An act the default branch already
|
|
722
|
+
satisfies exactly is kept with `satisfiedInBase: true` and writes nothing.
|
|
723
|
+
- Each set also writes the Launcher's guide, an `AGENTS.md` file inside
|
|
724
|
+
`clossys/`, which covers `clossys/` and the `clossys-*` skills (`agents-guide` item, write-record
|
|
725
|
+
source `agents-guide`, bound by code rule C9 to that one path). Its bytes are
|
|
726
|
+
the constant `AGENTS_GUIDE_TEXT`, the same for every repository: it says that
|
|
727
|
+
`clossys/` and the `clossys-*` skills (`.agents/skills/clossys-*`,
|
|
728
|
+
`.claude/skills/clossys-*`, `.cursor/skills/clossys-*`) are Launcher-owned
|
|
729
|
+
and are not edited, renamed or duplicated, and that the repository's own
|
|
730
|
+
skill policy carves that namespace out. Ownership is the ledger row's digest
|
|
731
|
+
of the whole file, like any other whole file, with no marker inside it: an
|
|
732
|
+
existing guide file the ledger does not record is refused as
|
|
733
|
+
`unowned-existing`, and only a setup set adopts one whose bytes are already
|
|
734
|
+
exactly the guide. A repository set up before the guide existed has neither
|
|
735
|
+
a ledger row nor a file there, so an apply set adds the guide. The planner
|
|
736
|
+
adds it only where the trusted ledger has no row for it and nothing, file or
|
|
737
|
+
directory, is at its path in any letter case. Admission accepts that one
|
|
738
|
+
add and no other only where the setup set wrote no guide and the base tree
|
|
739
|
+
at the set's base commit holds nothing at its path in any letter case. The
|
|
740
|
+
ledger's succession rule S3 reads the two ledgers, not the tree: it accepts
|
|
741
|
+
the one new row only where the base ledger has none at that path, with mode
|
|
742
|
+
`100644` and the digest of `AGENTS_GUIDE_TEXT` as its `after`.
|
|
743
|
+
`verify` compares the head's bytes with the set's digest
|
|
744
|
+
and with `verifyAgentsGuide()`; any difference is `diverged`, and `status`
|
|
745
|
+
reports it as `agents-guide-mismatch`, printing only that token and the pull
|
|
746
|
+
request number, never file text. The repository's own root `AGENTS.md` and
|
|
747
|
+
`CLAUDE.md` are not touched. A follow-up will append one fixed pointer line
|
|
748
|
+
to the root `AGENTS.md`, only when it is absent, and never rewrite the
|
|
749
|
+
file.
|
|
750
|
+
- A skill under a symbolic link on the default branch (`.agents`,
|
|
751
|
+
`.agents/skills` or the role's own skill directory) is refused as
|
|
752
|
+
`skills-root-is-link`: the planner never writes through a link.
|
|
753
|
+
- A repository in the `setup` phase gets a setup set, which the change-set
|
|
754
|
+
contract requires to carry exactly one item for each of four setup
|
|
755
|
+
templates (`caller-workflow`, `starter-request`, `ci-template` and
|
|
756
|
+
`path-scope-job`), the plan's one Starter pin, and, for pnpm, one
|
|
757
|
+
release-age exemption. The template bytes come only from
|
|
758
|
+
`renderSetupTemplate()`; the template patterns join `pathAllowList` before
|
|
759
|
+
any template file is written, and the Starter request takes the package
|
|
760
|
+
manager, the repository id and the plan's pin and nothing else. A template
|
|
761
|
+
file the base does not have is created; one it has is adopted only when its
|
|
762
|
+
bytes are exactly the set's own, and is `unowned-existing` otherwise.
|
|
763
|
+
Adoption follows the same compare-and-swap table as any whole file, and is
|
|
764
|
+
the one place the generation-0 adoption pass runs: a composed skill is
|
|
765
|
+
adopted only when the base's skills manifest records the digest of its
|
|
766
|
+
bytes. The plan's `pin-starter` act is the set's Starter pin. Every
|
|
767
|
+
`install` act goes to `deferred` with the reason `after-setup`: it gets no
|
|
768
|
+
item, no key and no lockfile invariant, and is written by the apply set
|
|
769
|
+
that follows.
|
|
770
|
+
- For pnpm, a setup set carries one `exempt-release-age` item, with the fixed
|
|
771
|
+
item id `release-age`, for the pnpm workspace file. Its text comes only from
|
|
772
|
+
`editReleaseAgeExemption()`, over the exact text of `pnpm-workspace.yaml`
|
|
773
|
+
and of `.npmrc` that the observation carries. An edit is a whole-file write
|
|
774
|
+
whose `before` is the digest the base has (or null, when the file is
|
|
775
|
+
created); an entry the file already lists gives the item and no file; a
|
|
776
|
+
file the editor will not read, or one whose `.npmrc` sets the same list,
|
|
777
|
+
gives a path refusal with the editor's reason
|
|
778
|
+
(`release-age-surface-unparseable`, `release-age-surface-conflict`) and a V6
|
|
779
|
+
`indeterminate` check with the same rule. A directory at the file's path is
|
|
780
|
+
refused as unparseable. An apply set carries the same item, with no file,
|
|
781
|
+
when its trusted ledger records that entry, so that it matches the setup
|
|
782
|
+
set item for item. npm has no exemption key, so an npm set has no item.
|
|
783
|
+
- A repository is skipped, `indeterminate` and outside the bundle digest,
|
|
784
|
+
with a reason of its own, wherever a setup set cannot be computed safely:
|
|
785
|
+
`package-manager-unsupported` (neither npm nor pnpm), `starter-pin-absent`
|
|
786
|
+
(the plan names no single Starter pin there), `starter-pin-unsupported`
|
|
787
|
+
(a pin outside the templates' range, `STARTER_PIN_RANGE`),
|
|
788
|
+
`starter-request-invalid` (a request the renderer refuses, such as a
|
|
789
|
+
repository id that is not `owner/name`), and `release-age-text-absent` (a
|
|
790
|
+
pnpm repository whose workspace file or `.npmrc` is there and whose exact
|
|
791
|
+
text the observation does not carry). An apply set whose Starter pin would
|
|
792
|
+
write a key is skipped as `starter-request-stale`, because the request a
|
|
793
|
+
setup set wrote would then name another pin and an apply set does not
|
|
794
|
+
rewrite it. The planner throws if the text an observation carries is not
|
|
795
|
+
the file its `files` digest.
|
|
796
|
+
- The planner reads the repository's installed-state ledger at the base
|
|
797
|
+
and trusts it only through change sets the hub holds (see
|
|
798
|
+
`trustInstalledLedger()` above): every generation must be a held set whose
|
|
799
|
+
digest recomputes and that agrees with its history entry, and every row a
|
|
800
|
+
write of the set it names. A ledger that fails skips the repository,
|
|
801
|
+
`indeterminate`, with the trust rule as its reason (`ledger-unreadable`,
|
|
802
|
+
`identity`, `renamed`, `ledger-chain` or `ledger-foreign-row`), outside the
|
|
803
|
+
bundle digest. No ledger is generation 0. The set's `ledger.generation`
|
|
804
|
+
comes from the trusted ledger.
|
|
805
|
+
- Each file the set writes whole is computed by compare-and-swap against
|
|
806
|
+
the trusted ledger's row at its path: with no row, an absent path is added
|
|
807
|
+
and a present one is refused as `unowned-existing`; with a row, a base that
|
|
808
|
+
still holds the row's bytes is kept (`before` equal to `after`) or updated,
|
|
809
|
+
a base with other bytes is refused as `client-edited`, and an absent one as
|
|
810
|
+
`deleted`, never folded into an update; other paths still proceed. Taking
|
|
811
|
+
over a present file with no row (the generation-0 adoption pass) is
|
|
812
|
+
allowed only in a setup set, never in an apply set. A `package.json` key
|
|
813
|
+
follows the same table against the ledger's key row; a key whose row, base
|
|
814
|
+
value and desired version agree while the lockfile does not resolve that
|
|
815
|
+
version and integrity skips the repository as `integrity-mismatch`,
|
|
816
|
+
`violated`, and is never repaired. The lockfile and the ledger are derived
|
|
817
|
+
files, checked by their invariants, and are not refused this way.
|
|
818
|
+
- In an apply set, each setup template whose files all have ledger rows is
|
|
819
|
+
carried as a no-op item, one keep entry per file (or `client-edited` /
|
|
820
|
+
`deleted`); a template with only some of its files in the ledger skips the
|
|
821
|
+
repository as `template-rows-partial`. A set edits the Controller repository
|
|
822
|
+
profile: when the observation carries
|
|
823
|
+
its text and the edit is stable, a `declare-root-entry` item adds each root
|
|
824
|
+
name the set introduces and the vocabulary lacks, as an allowed extension,
|
|
825
|
+
changing nothing else in the profile (`editJsonPointer`, the same editor
|
|
826
|
+
materialize uses for `package.json` keys). When that text is absent, the
|
|
827
|
+
repository is skipped as `root-entry-edit-unbuilt`. A profile it cannot read
|
|
828
|
+
(`root-vocabulary-unknown`), or one that prohibits a root name the set
|
|
829
|
+
introduces (`root-entry-prohibited`), gets the item refused instead; a
|
|
830
|
+
profile that already declares every name, or checks no root vocabulary,
|
|
831
|
+
gets no item.
|
|
832
|
+
- A trusted row the desired state no longer names (a role no longer
|
|
833
|
+
staffed, a package act the plan no longer names there) is left in place,
|
|
834
|
+
and the set reports V8 `indeterminate` with rule `removal-unbuilt`: removal
|
|
835
|
+
sets are not built.
|
|
836
|
+
- The change-set digest leaves out what is computed from it or from what it
|
|
837
|
+
covers -- the digest itself, the branch, the bundle digest, the pull
|
|
838
|
+
request text and the inverse set -- and `tooling`, which records the
|
|
839
|
+
machine, and `texts`, which holds whole-file bytes for materialize only.
|
|
840
|
+
It also leaves out a derived file's `before` and `after`, for two
|
|
841
|
+
different reasons: the ledger's bytes cite the digest, and a lockfile's
|
|
842
|
+
bytes depend on the package manager's version, so both are checked by
|
|
843
|
+
their invariants, which stay covered. Only those two files may be
|
|
844
|
+
derived. So a moved base, a different Launcher version, a visibility
|
|
845
|
+
change, a staffing change, different package bytes or a different ledger
|
|
846
|
+
generation is a new set, and recomputing any excluded value is not.
|
|
847
|
+
- Every array whose order carries no meaning is written in one canonical
|
|
848
|
+
order, and the contract refuses any other order, so observing the same
|
|
849
|
+
repository twice, in any order, gives the same bytes and the same digest.
|
|
850
|
+
The contract's code rules also tie each kind of item the planner computes
|
|
851
|
+
to exactly what it writes; the acts nothing computes yet are declared but
|
|
852
|
+
not yet tied to their files.
|
|
853
|
+
- The bundle digest covers only the plan digest and each computed
|
|
854
|
+
repository's id and change-set digest, so an approval can bind it and a
|
|
855
|
+
repository can recompute it from digests alone.
|
|
856
|
+
- The planner's bundle `mode` is `report`, and it records no repository
|
|
857
|
+
state and no binding. The bundle contract also defines a `planned` mode,
|
|
858
|
+
where a repository that passed all nine pre-apply checks and is bound by an
|
|
859
|
+
approval is `planned`, with that binding; the `plan` command writes one
|
|
860
|
+
only when the hub's committed plan carries an approval that names a bundle
|
|
861
|
+
the hub holds (see the plan command below). The planner reports its
|
|
862
|
+
own dry-materialization check (V6), which covers the file
|
|
863
|
+
layout only: the part of V6 that regenerates the lockfile and checks its
|
|
864
|
+
invariants is not run by the planner, so a set that changes a lockfile carries V6
|
|
865
|
+
`indeterminate` with rule `lockfile-not-run`, and V6 is `satisfied` only
|
|
866
|
+
for a set with no lockfile change; the `plan` command replaces that entry
|
|
867
|
+
by running the rest of V6, and V9, in a temporary tree (see the plan
|
|
868
|
+
command below). A release-age path refusal adds V6
|
|
869
|
+
`indeterminate` with its reason as the rule. Compare-and-swap outcomes are reported under V8:
|
|
870
|
+
`unowned-existing`, `client-edited`, `deleted` and `removal-unbuilt` each
|
|
871
|
+
give V8 `indeterminate`, and a set with none of them gives V8 `satisfied`. Two V3 checks
|
|
872
|
+
need no observation: when the authorization names a different plan
|
|
873
|
+
digest than the plan's, every computed repository gets a violated V3
|
|
874
|
+
check (`authorization-plan-mismatch`), and when the plan has package acts
|
|
875
|
+
and no authorization is given, every computed repository gets a violated
|
|
876
|
+
V3 check (`authorization-absent`). Each repository's verdict is the worst
|
|
877
|
+
of its checks.
|
|
878
|
+
|
|
879
|
+
What a setup set guarantees: it validates against the contract, every write
|
|
880
|
+
is a compare-and-swap against the base, a file is adopted only by byte proof,
|
|
881
|
+
template bytes come from the renderer alone, the Starter pin is one the
|
|
882
|
+
templates support, and materialization and verification both prove that the
|
|
883
|
+
release-age file is the base's bytes plus exactly the one scope entry. What it
|
|
884
|
+
does not handle: root entries in a setup set (the apply set that follows is
|
|
885
|
+
refused by admission because its items differ), lockfile regeneration and the
|
|
886
|
+
provenance check (V9), which the `plan` command runs on a temporary tree, and
|
|
887
|
+
a later change of the pin.
|
|
888
|
+
|
|
889
|
+
Nothing here writes to a product repository, creates a branch or opens a
|
|
890
|
+
pull request; the only files this part of the package writes are the hub's
|
|
891
|
+
change-set and bundle stores (see the ledger section below). Reading the
|
|
892
|
+
repositories, branch creation, exact package installs,
|
|
893
|
+
adding Starter's caller workflow, and opening one pull request per staffed
|
|
894
|
+
repository -- including the setup pull request that brings a staffed
|
|
895
|
+
repository `@clossys-advisor` and the voices of the roles staffed there --
|
|
896
|
+
are not built yet, so until it ships no Launcher command puts those voices
|
|
897
|
+
into a product repository.
|
|
898
|
+
|
|
899
|
+
### Observing a repository
|
|
900
|
+
|
|
901
|
+
`observeRepository({ id, clone, hubOwner?, ports })` turns one local clone
|
|
902
|
+
into the `RepositoryObservation` that `planApplyBundle()` takes, or into a
|
|
903
|
+
skipped observation `{ id, skipped, verdict }` with a reason id. The input is
|
|
904
|
+
an `ObserveRepositoryInput`: a bare `id` is qualified by `hubOwner`, and the
|
|
905
|
+
`RepositoryObservationPorts` supply the two values a clone cannot hold
|
|
906
|
+
(`nodeId` and `visibility`) and, optionally, `originId`, which maps an origin
|
|
907
|
+
URL to `owner/name`.
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
const observed = await observeRepository({
|
|
911
|
+
id: "acme/site",
|
|
912
|
+
clone: "/work/site",
|
|
913
|
+
ports: { nodeId, visibility },
|
|
914
|
+
});
|
|
915
|
+
if ("skipped" in observed) console.log(observed.skipped, observed.verdict);
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
- The rule: only this repository's own object database and refs are read,
|
|
919
|
+
through git plumbing, never the working tree and never an alternate object
|
|
920
|
+
store, `commondir` or submodule repository; anything unusual is refused,
|
|
921
|
+
not interpreted. What the object database holds is trusted to match its
|
|
922
|
+
ids: an observation is the committed head as the clone's object database
|
|
923
|
+
stores it. Every field is read from git objects at the default-branch
|
|
924
|
+
head; a file's digest is that of its bytes read as UTF-8 text, as
|
|
925
|
+
materialization computes it (a symbolic link's digest is the digest of its
|
|
926
|
+
target). Nothing is written to the clone: the remote tip is read with
|
|
927
|
+
`git ls-remote` run outside the clone, and the default branch comes from
|
|
928
|
+
the remote's `HEAD`.
|
|
929
|
+
- The committed tree is listed first, and a submodule is refused
|
|
930
|
+
(`submodule-present`), before any command that reads the working tree runs;
|
|
931
|
+
`git status` never considers a submodule, because git would open the
|
|
932
|
+
submodule's own repository and read its configuration.
|
|
933
|
+
- A clone is refused, not observed, when its directory is missing
|
|
934
|
+
(`clone-missing`, `indeterminate`); when its `origin` is another repository;
|
|
935
|
+
when its tree is dirty or has untracked files not ignored, or a tracked file is marked
|
|
936
|
+
skip-worktree or assume-unchanged; when its local head differs from the
|
|
937
|
+
remote tip; when it reads objects from another store
|
|
938
|
+
(`objects/info/alternates`); when its git directory holds a split index's
|
|
939
|
+
shared file (`sharedindex.*`), which git rewrites on every index read, so
|
|
940
|
+
observing it would write to the clone (`clone-config-unsafe`); or when `.git/config` holds a key outside a
|
|
941
|
+
short fixed list (`violated`). The config is read as data, so a filter,
|
|
942
|
+
hook path, pager, `fsmonitor` or alias entry is refused rather than run.
|
|
943
|
+
- The default origin parser names only an exact `https://github.com/` or
|
|
944
|
+
`ssh` GitHub URL, and only those two transports fetch; an `originId` you
|
|
945
|
+
supply is the only way a local path is fetched. git is run from an absolute
|
|
946
|
+
path found among the absolute, non-empty `PATH` entries, never by a search
|
|
947
|
+
of the clone's own directory.
|
|
948
|
+
- `files` lists whatever the head holds at a path the apply flow may write: a
|
|
949
|
+
file, a symbolic link, or, for a directory, each file under it, so a
|
|
950
|
+
directory where a link belongs reads as occupied. Root entries that differ
|
|
951
|
+
from `clossys`, `.agents`, `.claude` or `.cursor` only by letter case,
|
|
952
|
+
Unicode form or a trailing dot, and two spellings of `.github` or
|
|
953
|
+
`.starter`, are refused (`case-variant-owned-path`); a `consumerCi` workflow
|
|
954
|
+
is a regular file spelled `.github/workflows/`. A regular `.npmrc` is listed
|
|
955
|
+
as well, though the flow never writes it, because a pnpm setup reads it.
|
|
956
|
+
- `pnpmWorkspaceText` and `npmrcText` are the exact UTF-8 text of
|
|
957
|
+
`pnpm-workspace.yaml` and `.npmrc` at the head, or null when the file is
|
|
958
|
+
absent or is not UTF-8; the planner reads a release-age exemption only from
|
|
959
|
+
them, and throws when one is not the file `files` digests.
|
|
960
|
+
- `nodeId` and `visibility` come through the injected ports; a port that
|
|
961
|
+
throws or returns a malformed value is `indeterminate`.
|
|
962
|
+
- `phase` is `apply` only when the base has a valid installed-state ledger,
|
|
963
|
+
every setup-template path is a regular file at the head, and
|
|
964
|
+
`manifestEntries` pins `@clossys/starter` at an exact `0.2.x` version, the
|
|
965
|
+
range the setup templates support (the templates' own predicate; `0.1.9`
|
|
966
|
+
and `0.3.0` read as `setup`), for which its lockfile has a row of that name
|
|
967
|
+
and version, not an alias (an `npm:` alias,
|
|
968
|
+
or another package under its name, is refused as `lockfile-unreadable`; the
|
|
969
|
+
host and integrity are the planner's to check); otherwise it is `setup`.
|
|
970
|
+
- git runs without hooks, `fsmonitor` or a pager, and every tree, blob and
|
|
971
|
+
output read has a size bound. git inside the clone reads no configuration
|
|
972
|
+
but the vetted `.git/config` and fixed `-c` overrides: the system and
|
|
973
|
+
global configuration are switched off, and `GIT_CONFIG_COUNT` and its
|
|
974
|
+
key and value variables, `GIT_CONFIG_PARAMETERS`, `GIT_CONFIG_SYSTEM` and
|
|
975
|
+
`GIT_ATTR_SOURCE` are removed. `git ls-remote`, which runs outside the
|
|
976
|
+
clone, keeps the operator's global and system git config files (credential
|
|
977
|
+
helpers, proxy) and is given `GIT_CONFIG_COUNT`, `GIT_CONFIG_KEY_n` and
|
|
978
|
+
`GIT_CONFIG_VALUE_n`, so an origin that needs credentials supplied through
|
|
979
|
+
them is observed; a fixed `-c` override and `GIT_ALLOW_PROTOCOL` still win
|
|
980
|
+
over them. `GIT_CONFIG_PARAMETERS` (git's internal encoding of `-c`, not an
|
|
981
|
+
interface) and `GIT_CONFIG_SYSTEM` are not forwarded to it, so an origin that
|
|
982
|
+
needs either is skipped as `remote-tip-unreadable`. git's own `GIT_TEST_*`
|
|
983
|
+
switches (`GIT_TEST_SPLIT_INDEX` among them) reach no git command.
|
|
984
|
+
|
|
985
|
+
| Verdict | Skip reasons |
|
|
986
|
+
| --- | --- |
|
|
987
|
+
| `violated` | `invalid-id`, `clone-config-unsafe`, `origin-mismatch`, `not-on-default-branch`, `remote-tip-mismatch`, `working-tree-dirty`, `package-manager-conflict` |
|
|
988
|
+
| `indeterminate` | `clone-missing`, `clone-unreadable`, `id-owner-unknown`, `remote-tip-unreadable`, `node-id-unavailable`, `visibility-unavailable`, `tree-too-large`, `submodule-present`, `manifest-unreadable`, `lockfile-ambiguous`, `package-manager-unknown`, `lockfile-unreadable`, `release-age-surface-invalid`, `agents-link-unreportable`, `observation-too-large`, `case-variant-owned-path`, `ledger-unreadable`, `profile-ambiguous` |
|
|
989
|
+
|
|
990
|
+
What it does not decide: it reports what the committed head holds, not
|
|
991
|
+
whether applying is safe. The checks (configuration, head, index, status)
|
|
992
|
+
and the reads run one after another and are not repeated; the clone is not
|
|
993
|
+
locked. A process that changes the clone between a check and a read is not
|
|
994
|
+
detected, and cannot be detected by checking again, since any later check
|
|
995
|
+
has the same gap, and a process that can write the clone's `.git` can
|
|
996
|
+
already plant a hook or filter that the operator's own git would run. What
|
|
997
|
+
the observation reports is unaffected: every byte comes from an object at the
|
|
998
|
+
remote tip's commit, which a later commit, checkout or edit in the clone does
|
|
999
|
+
not change, so an observation states what that commit held, not that the
|
|
1000
|
+
clone stayed clean after it was checked. Object contents, and links inside
|
|
1001
|
+
`.git/objects`, are trusted to match their ids. Ignored files at owned paths
|
|
1002
|
+
read as absent (`base-mismatch` catches them); a symbolic link at `.github`,
|
|
1003
|
+
`.starter` or `clossys` is unreported here and refused by materialization
|
|
1004
|
+
(`symlink-ancestor`); conversion attributes (`eol`, `ident`, LFS) end in
|
|
1005
|
+
`base-mismatch`; lossy UTF-8 digests can collide, as in materialization; a
|
|
1006
|
+
clone clean only through a custom global `core.excludesFile`, or one that
|
|
1007
|
+
needs `safe.directory`, is refused, which fails closed. Ownership and trust
|
|
1008
|
+
are the planner's judgement, and so is whether the pinned Starter version implements the
|
|
1009
|
+
request beyond that template range.
|
|
1010
|
+
It is not a check of Windows short names or other alias spellings beyond case
|
|
1011
|
+
and Unicode-normalization folding, and which root names a particular set
|
|
1012
|
+
creates is the planner's contract check; the observation reports over
|
|
1013
|
+
`clossys`, `.agents`, `.claude` and `.cursor`, and, for a repository in its
|
|
1014
|
+
setup phase, `.github`, `.starter` and, for a pnpm repository whose workspace
|
|
1015
|
+
file the exemption edit will create, `pnpm-workspace.yaml`.
|
|
1016
|
+
|
|
1017
|
+
### The installed-state ledger
|
|
1018
|
+
|
|
1019
|
+
Every change set names `clossys/.state/installed.json` as a derived file:
|
|
1020
|
+
the ledger of what the apply flow wrote in that repository, one generation
|
|
1021
|
+
per merged change set (#1178). Its shape, its code rules, the bytes a
|
|
1022
|
+
change set's generation renders to, and the succession rules are the
|
|
1023
|
+
shared contract `docs/contracts/installed-ledger.json`, with a corpus
|
|
1024
|
+
computed independently of this package (both in the public repository, not
|
|
1025
|
+
shipped in this package). This package validates a ledger against it
|
|
1026
|
+
(`validateInstalledLedger()`), compares a pull request's ledger with its
|
|
1027
|
+
base's from their exact bytes (`ledgerSuccession()`), and gives a valid ledger its exact bytes
|
|
1028
|
+
(`serializeInstalledLedger()`), reads one strictly from its bytes, a `Uint8Array`,
|
|
1029
|
+
(`readInstalledLedger()`), renders the next generation a change set writes
|
|
1030
|
+
(`renderInstalledLedger()`), and decides whether a ledger may be trusted
|
|
1031
|
+
against the change sets the hub holds (`trustInstalledLedger()`). Nothing
|
|
1032
|
+
here reads a ledger from a repository: the caller hands its bytes in.
|
|
1033
|
+
|
|
1034
|
+
- Each generation records the change set that wrote it and its binding:
|
|
1035
|
+
`approved`, with the digest of the bundle the founder approved (which
|
|
1036
|
+
may be an earlier run's bundle than the one the set was computed in), or
|
|
1037
|
+
`admitted`, for the apply set that follows an approved setup set under
|
|
1038
|
+
one approval, naming that setup set. An admitted generation must come
|
|
1039
|
+
right after its setup generation, from the same plan and the same
|
|
1040
|
+
approved bundle.
|
|
1041
|
+
- No string in a valid ledger is plan or brief text: a `planItem` is exactly
|
|
1042
|
+
the repository id, a colon and the package name, a role appears only as a
|
|
1043
|
+
lowercase id token in a skill path, and a root entry is one of the fixed
|
|
1044
|
+
names the flow's own paths can introduce.
|
|
1045
|
+
- A valid ledger is a claim, not evidence. `trustInstalledLedger()` trusts
|
|
1046
|
+
it only when every generation is a change set the hub holds, valid and
|
|
1047
|
+
self-verifying, agreeing with its history entry, and every row is a write
|
|
1048
|
+
of the set it names; one row it cannot account for refuses the whole
|
|
1049
|
+
repository, because rendering the next generation would carry that row
|
|
1050
|
+
forward.
|
|
1051
|
+
- `renderInstalledLedger()` gives the bytes of the generation a change set
|
|
1052
|
+
writes over the previous ledger, and refuses a set computed from another
|
|
1053
|
+
generation or for another repository, and an apply set that keeps a file
|
|
1054
|
+
the previous ledger has no row for: an apply generation leaves the
|
|
1055
|
+
ledger's files unchanged.
|
|
1056
|
+
- The hub keeps every change set and bundle it computes under
|
|
1057
|
+
`clossys/.state/apply/change-sets/` and `clossys/.state/apply/bundles/`,
|
|
1058
|
+
one file per digest (`storeChangeSet()`, `storeApplyBundle()`). A stored
|
|
1059
|
+
change set is never replaced; a stored bundle is replaced atomically, and
|
|
1060
|
+
only when the same `bundleDigest` is stored again, which covers the plan
|
|
1061
|
+
digest and the change-set digests only, so only the authorization, verdict,
|
|
1062
|
+
clock and mode fields can differ. A rerun replaces a stored bundle of the
|
|
1063
|
+
same digest, planned or not, with one exception: a report-mode bundle never
|
|
1064
|
+
replaces a stored planned one that verifies (`store-failed`), so an
|
|
1065
|
+
approval-bound record is not downgraded by a run that lost the approval. A
|
|
1066
|
+
stored file that does not verify is replaced. A read returns a file only
|
|
1067
|
+
when its recomputed digest matches its name.
|
|
1068
|
+
- The succession rules compare what two ledgers claim, not the files: an
|
|
1069
|
+
admitted generation must install exactly the packages its setup deferred
|
|
1070
|
+
and change no other row (an apply set may also add the one guide row, for
|
|
1071
|
+
the `AGENTS.md` file inside `clossys/`, with the guide's digest), but whether the pull request's
|
|
1072
|
+
tree matches its ledger is a separate check.
|
|
1073
|
+
- For a reader without the hub, an `approved` head generation is an
|
|
1074
|
+
unauthenticated claim, never an admission or a pass: a pull request
|
|
1075
|
+
could relabel an admitted generation `approved` to escape the admission
|
|
1076
|
+
rules. `ledgerSuccession()` therefore reports such a head as
|
|
1077
|
+
`approval-claimed`, and `admitted` only for an admitted generation that
|
|
1078
|
+
every rule proved. A caller that decides without the hub refuses an
|
|
1079
|
+
`approval-claimed` head on an apply pull request, or treats the pull
|
|
1080
|
+
request as one that needs the client's own review.
|
|
1081
|
+
- Each ledger is read from its bytes and must be exactly the bytes the
|
|
1082
|
+
contract renders for it: a repeated key, a byte order mark or any other
|
|
1083
|
+
spelling is refused (`bytes`), never read as an unchanged ledger.
|
|
1084
|
+
|
|
1085
|
+
### Planning and the approval sheet
|
|
1086
|
+
|
|
1087
|
+
`launcher-apply-plan plan` runs in the hub and takes no option beyond `--help`.
|
|
1088
|
+
It computes the apply bundle for the plan file in the hub's working tree, reports whether
|
|
1089
|
+
that exact file is the one committed at `HEAD` (the sheet's `Plan committed:` line), and prints the
|
|
1090
|
+
approval sheet a client reads before approving it (RFC section 12.7). It
|
|
1091
|
+
reads the hub only: `clossys/advisor/plan.json` and `brief.json`, the composed
|
|
1092
|
+
skill of each staffed role and of the Advisor voice, the stored change sets,
|
|
1093
|
+
the inventory (a staffed repository the inventory does not list is skipped as
|
|
1094
|
+
`not-in-inventory`), the exact `@clossys/advisor` and `@clossys/integrator`
|
|
1095
|
+
versions in `package.json` with their integrity from the lockfile (a range, or
|
|
1096
|
+
a version the lockfile does not hold, is refused), and the execution
|
|
1097
|
+
authorization committed in `clossys/advisor/assessment-input.json`, read as
|
|
1098
|
+
the blob at `HEAD` and never from the working tree. No blob, or no
|
|
1099
|
+
`engagement.executionAuthorization`, is no authorization; one that is not an
|
|
1100
|
+
object, or lacks a string `planDigest` or `expiresAt`, is refused. Each staffed
|
|
1101
|
+
repository is observed from its clone, a sibling of the hub, by
|
|
1102
|
+
`observeRepository()`, and `planApplyBundle()` does the rest. No option carries
|
|
1103
|
+
an approval or a binding. The result is a `planned` bundle only when the plan
|
|
1104
|
+
is the committed one and the hub's committed plan, read as a git object at an
|
|
1105
|
+
attached `HEAD`, carries an approving decision for this plan digest whose
|
|
1106
|
+
subject is a bundle the hub stores and verifies. Then each repository's V3 is
|
|
1107
|
+
decided by the admission check (`decideSetBinding()`, over the installed-state
|
|
1108
|
+
ledger at the set's base commit), and only a bound repository gets a binding:
|
|
1109
|
+
V3 `violated` (exit 1) or `indeterminate` (exit 2) with its fixed rule token
|
|
1110
|
+
gives none, and a repository whose V3 the bundle already marks violated is
|
|
1111
|
+
not admitted and spawns no readiness run. A repository with a binding and
|
|
1112
|
+
V1 to V9 all satisfied is `planned`; V1, V2, V4, V5 and V7 are recorded
|
|
1113
|
+
satisfied, V9 satisfied only for a set with no derived lockfile (a set that
|
|
1114
|
+
changes one keeps the dry tree's V9, and is not `planned` without it). An apply
|
|
1115
|
+
set names the bundle that will be recorded in the ledger, and admission needs
|
|
1116
|
+
that bundle stored, so the first run after a setup set merges stores it with
|
|
1117
|
+
V3 `indeterminate` (`apply-bundle-unrecorded`) and the next run admits it. The
|
|
1118
|
+
digests and the change sets are the same as in report mode. So is the sheet,
|
|
1119
|
+
apart from its `Mode:` line and, under `Checks not satisfied`, the V3 row of a
|
|
1120
|
+
repository the hub refuses (and the V9 row of a set that changes a lockfile, when
|
|
1121
|
+
the dry tree ran no provenance check), which only planned mode adds.
|
|
1122
|
+
|
|
1123
|
+
It then stores the change sets and the bundle under `clossys/.state/apply/`,
|
|
1124
|
+
the only place it writes, and prints the sheet. Over unchanged inputs and an
|
|
1125
|
+
unchanged clock the sets, their digests and the bundle digest are the same on
|
|
1126
|
+
every run; the V6 and V9 checks stored with them come from the package manager,
|
|
1127
|
+
the registry and the Integrator, and the digest does not cover them. A rerun
|
|
1128
|
+
after the clock, the committed authorization or the result of a check changed
|
|
1129
|
+
keeps the same bundle digest, which covers the plan digest and the change-set
|
|
1130
|
+
digests only, and stores the newest computation under it: the one bundle file
|
|
1131
|
+
is replaced atomically, and the sheet is printed as before. Change sets stay
|
|
1132
|
+
append-only. Earlier computations under a digest are not recorded.
|
|
1133
|
+
|
|
1134
|
+
```text
|
|
1135
|
+
Approve subjectDigest: sha256:<the bundle digest>
|
|
1136
|
+
|
|
1137
|
+
| Repository | Kind | Item | Change | Digest |
|
|
1138
|
+
| --- | --- | --- | --- | --- |
|
|
1139
|
+
| example-owner/site | pin-starter | example-owner/site:@clossys/starter | @clossys/starter@0.2.0 | 3d2b4f88280d |
|
|
1140
|
+
| example-owner/site | compose-skills | skills | 10 paths | 3d2b4f88280d |
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
The sheet is ids and digests only: the bundle and plan digests, the mode, the
|
|
1144
|
+
authorization, one row for each item of each change set (`Change` is
|
|
1145
|
+
`name@version` for a package act, else a path count; `Digest` is the first 12
|
|
1146
|
+
hex digits of the set's digest), and then each deferred, refused or skipped
|
|
1147
|
+
entry by id or path and reason token. It carries no brief or plan prose, no
|
|
1148
|
+
stored text, no key value and no file contents. `renderApprovalSheet()` in
|
|
1149
|
+
this package builds it as a pure function of the bundle and its change sets,
|
|
1150
|
+
and refuses, by a fixed token that names no value, a set or bundle that does
|
|
1151
|
+
not validate, sets that are not exactly the bundle's, and any printed value
|
|
1152
|
+
that fails a strict pattern or holds `<`, `>`, a backtick, `|`, a carriage
|
|
1153
|
+
return or a line feed. Every refusal of the command is likewise a fixed token,
|
|
1154
|
+
printed as `launcher-apply-plan plan: <token>; nothing was stored`, except
|
|
1155
|
+
`store-failed` and an unexpected failure, which end without that clause because
|
|
1156
|
+
some of the sets may already be stored.
|
|
1157
|
+
|
|
1158
|
+
Exit `0` when every repository is `satisfied`; `1` when any is `violated`;
|
|
1159
|
+
`2` when an input could not be read, the planner refused, or a repository is
|
|
1160
|
+
`indeterminate`. A bundle that was computed is stored and printed whatever the
|
|
1161
|
+
exit. A Starter pin or a package install changes a lockfile, whose
|
|
1162
|
+
regeneration (V6) the planner does not run, so the command dry-materializes
|
|
1163
|
+
each such repository before it prints the sheet. It reads the clone at the
|
|
1164
|
+
set's base commit through git's object database (`ls-tree` and `cat-file`; no
|
|
1165
|
+
checkout, worktree, index or ref is written), writes that tree and the set's
|
|
1166
|
+
changes into a directory of its own under the operating system's temporary
|
|
1167
|
+
directory, regenerates the lockfile there with the runner `materialize` uses
|
|
1168
|
+
(V6), and, only when that passes, runs the hub's provenance check (V9) on the
|
|
1169
|
+
same tree. The directory is removed before the command returns, whatever
|
|
1170
|
+
happened. A submodule, a link that could reach outside the tree, a link or file
|
|
1171
|
+
whose name a filesystem that folds case or normalization would read as a parent
|
|
1172
|
+
directory of another entry, a `..` or `.git` path segment or a tree over the
|
|
1173
|
+
size cap gives V6 `indeterminate` and launches nothing. A base file whose bytes
|
|
1174
|
+
differ from what the set names as its `before` gives V6 `violated`
|
|
1175
|
+
(`base-mismatch`), and set text whose bytes differ from its digest gives V6
|
|
1176
|
+
`indeterminate` (`change-set-invalid`). A failure of the dry tree gives V6
|
|
1177
|
+
`indeterminate` with rule `dry-tree-failed` for that repository only, and V9 is
|
|
1178
|
+
then `indeterminate` with rule `lockfile-not-regenerated`. Only V6 and V9 change: the sets, their
|
|
1179
|
+
digests and the bundle digest do not. A repository that has a refused path or
|
|
1180
|
+
key, or another check that is not satisfied, is not dry-materialized. No rule
|
|
1181
|
+
carries tool output, a path or an id. The tool version of pnpm and Yarn is not
|
|
1182
|
+
supplied, so a repository that uses one stays `indeterminate`. Supersede is
|
|
1183
|
+
not part of this command.
|
|
1184
|
+
|
|
1185
|
+
### Materializing and verifying a change set
|
|
1186
|
+
|
|
1187
|
+
`launcher-apply-plan materialize --repo <id>` writes a stored repository
|
|
1188
|
+
change set into that repository's local clone on the branch the set names,
|
|
1189
|
+
including the installed-state ledger. `launcher-apply-plan verify --repo <id>`
|
|
1190
|
+
re-reads the clone and reports whether the working tree still matches the
|
|
1191
|
+
set. Both commands are step 3 in
|
|
1192
|
+
[`docs/rfcs/apply-approved-plan.md`](../../docs/rfcs/apply-approved-plan.md)
|
|
1193
|
+
(section 11); a successful verify corresponds to the `materialized` row in
|
|
1194
|
+
section 4.4 of that RFC.
|
|
1195
|
+
|
|
1196
|
+
### What authorizes a write
|
|
1197
|
+
|
|
1198
|
+
`materialize` and `verify` decide, from the hub alone, on whose authority a
|
|
1199
|
+
change set is written, and record exactly that in the ledger; no flag, option
|
|
1200
|
+
or default supplies it. The decision reads the plan committed at the hub's
|
|
1201
|
+
current branch head (an uncommitted edit to `clossys/advisor/plan.json` is
|
|
1202
|
+
ignored, and a detached head refuses), the latest approving decision's
|
|
1203
|
+
subject digest, the stored bundle with that digest, and the stored change
|
|
1204
|
+
sets.
|
|
1205
|
+
|
|
1206
|
+
- **Approved.** The set is a member of that bundle, by repository id and
|
|
1207
|
+
change-set digest, and its plan digest equals the plan's. The ledger
|
|
1208
|
+
records `approved` with the bundle's digest.
|
|
1209
|
+
- **Admitted.** An apply set that is not a member is admitted, with no second
|
|
1210
|
+
approval, only when all of the following hold: it has the same plan digest
|
|
1211
|
+
and the approving decision is still the latest; its package acts equal the
|
|
1212
|
+
setup set's by plan item, it defers nothing, has the same `producer`, and
|
|
1213
|
+
every whole-file entry is a no-op except the Launcher guide's add (the
|
|
1214
|
+
`agents-guide` item and its file, before null and after the guide's digest,
|
|
1215
|
+
only where the setup set wrote no guide and the base tree at the set's base
|
|
1216
|
+
commit holds nothing at its path in any letter case); and the base's
|
|
1217
|
+
trusted ledger ends with
|
|
1218
|
+
that setup set, bound `approved` to the same subject, with every byte the
|
|
1219
|
+
setup set wrote present in the base by content, so a squash or rebase merge
|
|
1220
|
+
is admitted. The setup set must itself be a member of the approved bundle,
|
|
1221
|
+
and the ledger the set would write must pass the succession rules as an
|
|
1222
|
+
admitted generation.
|
|
1223
|
+
- **Otherwise** the step reports `indeterminate` with reason
|
|
1224
|
+
`awaiting-approval` and a fixed detail token, and writes nothing.
|
|
1225
|
+
|
|
1226
|
+
The hub's head must be its branch's upstream. `readHubAuthority()` resolves
|
|
1227
|
+
`HEAD` once to a commit id, requires the attached branch's configured upstream
|
|
1228
|
+
to resolve to that same commit (local refs only, nothing is fetched), and reads
|
|
1229
|
+
the plan from that commit id. `HEAD`, its commit and its upstream are read by
|
|
1230
|
+
one `git rev-parse`, and the upstream must be a remote-tracking ref (under
|
|
1231
|
+
`refs/remotes/`). A branch with no upstream, an upstream that is a local
|
|
1232
|
+
branch, or one ahead of or behind it, is `indeterminate` with detail
|
|
1233
|
+
`hub-not-upstream`.
|
|
1234
|
+
|
|
1235
|
+
A set with package acts also needs a current execution authorization: the
|
|
1236
|
+
hub's own `node_modules/.bin/advisor-execution-readiness` (never `npx`) runs
|
|
1237
|
+
against the committed `clossys/advisor/assessment-input.json` at the current
|
|
1238
|
+
instant, and the authorization must name the plan digest, the repository and
|
|
1239
|
+
every package act. The assessment is read at the commit id the plan was read
|
|
1240
|
+
from: a hub whose `HEAD` has since moved, detached, or stopped matching its
|
|
1241
|
+
upstream is `indeterminate` with detail `hub-head-moved`. A set with no package
|
|
1242
|
+
acts skips this step, so its hub head is read once, when the plan is read, and
|
|
1243
|
+
not checked a second time. The authorization's permitted packages must also
|
|
1244
|
+
equal the plan's packages, by name, version and integrity (each compared as one
|
|
1245
|
+
unit, so an empty name cannot stand in for another entry) with each distinct
|
|
1246
|
+
package listed once; an extra, a missing or a repeated
|
|
1247
|
+
entry is `violated` with detail `packages-not-exact`. Readiness's own answer is
|
|
1248
|
+
kept: not current is `violated`; unreadable, absent or failing to run is `indeterminate`. `materialize` checks
|
|
1249
|
+
before its first write, and `verify` checks again, so a withdrawn approval or
|
|
1250
|
+
an expired authorization fails `verify`.
|
|
1251
|
+
|
|
1252
|
+
`readHubAuthority()` reads the committed approval, `planPackagesFor()` gives
|
|
1253
|
+
the plan's package identities for one repository, and `decideSetBinding()`
|
|
1254
|
+
returns the binding or an `AdmissionRefusal` (exit code, reason and a fixed
|
|
1255
|
+
detail token). `HubAuthority` is what `readHubAuthority()` returns: the plan,
|
|
1256
|
+
its digest, the approved subject, and `head`, the commit id the plan was read
|
|
1257
|
+
from. A `ReadinessRunner` replaces the process launch of the readiness executable, for
|
|
1258
|
+
tests.
|
|
1259
|
+
|
|
1260
|
+
This proves that the bytes are those the committed decision names, or that the
|
|
1261
|
+
one-approval rule admits. It does not prove who committed the decision; the
|
|
1262
|
+
hub repository's branch protection governs that.
|
|
1263
|
+
|
|
1264
|
+
### Rendering the pull request
|
|
1265
|
+
|
|
1266
|
+
`renderPullRequest({ set, binding, taskRecord, supersedes? })` returns the title and body of
|
|
1267
|
+
the pull request for one stored change set, and `bodySha256`, which is
|
|
1268
|
+
`sha256:` and the hex SHA-256 of the body's UTF-8 bytes. It is a pure function
|
|
1269
|
+
of its inputs (`RenderPullRequestInput`: the set, the binding, the task record and the optional superseded numbers): it reads no file, runs no command and opens nothing. The
|
|
1270
|
+
title is exactly `set.pullRequest.title`, which must be `Clossys: apply plan `
|
|
1271
|
+
and the first 12 hex digits of the set's own digest.
|
|
1272
|
+
|
|
1273
|
+
The body is LF only and ends in one LF. Its first line is the marker
|
|
1274
|
+
`<!-- clossys-change-set: sha256:<64 hex> -->`, and no other line is a marker.
|
|
1275
|
+
It then names the repository id, the phase, and the change-set, plan and bundle
|
|
1276
|
+
digests; the binding you pass (`approved` with its subject digest, or `admitted`
|
|
1277
|
+
with its subject digest and setup change set); one line per item, in the set's
|
|
1278
|
+
own order, with `name@version` for `install` and `pin-starter`; each deferred or
|
|
1279
|
+
refused entry by item id and reason code only; and a `## Task record` section
|
|
1280
|
+
that links `#<n>`. With `supersedes`, a list of distinct positive safe integers
|
|
1281
|
+
that are not the task record, it also writes a `## Supersedes` section before
|
|
1282
|
+
the task record, one `- #<n>` line for each number, ascending; without it, or
|
|
1283
|
+
with an empty list, the body is byte for byte what it was. It carries ids, act names, versions and digests only, never
|
|
1284
|
+
brief or plan prose, file contents, key values or paths.
|
|
1285
|
+
|
|
1286
|
+
`readChangeSetMarker(body)` returns the digest only when exactly one line of the
|
|
1287
|
+
body is exactly the marker and the marker appears nowhere else, and `null`
|
|
1288
|
+
otherwise.
|
|
1289
|
+
|
|
1290
|
+
It returns a `PullRequestText`, or a `PullRequestRefusal` whose
|
|
1291
|
+
`PullRequestRefusalReason` is a fixed token that names no id, digest or input
|
|
1292
|
+
text, for a set that fails `validateRepositoryChangeSet` or
|
|
1293
|
+
whose digest does not recompute, a malformed binding, an `admitted` binding on
|
|
1294
|
+
a setup set, a task record that is not a positive safe integer, a `supersedes` list that is
|
|
1295
|
+
not distinct positive safe integers other than the task record
|
|
1296
|
+
(`supersedes-invalid`), and any value it
|
|
1297
|
+
cannot prove safe to write (each must match its own strict pattern and hold no
|
|
1298
|
+
`<`, `>`, backtick, `|`, carriage return or line feed).
|
|
1299
|
+
|
|
1300
|
+
It does not decide the binding: it shows what you pass, so pass the result of
|
|
1301
|
+
`decideSetBinding()`. It does not check that the task-record issue exists, and
|
|
1302
|
+
it cannot stop a pull request's body being edited after it is opened; keeping
|
|
1303
|
+
`bodySha256` and the marker is what lets a later step notice that.
|
|
1304
|
+
|
|
1305
|
+
### Recording the body
|
|
1306
|
+
|
|
1307
|
+
`launcher-apply-plan body --repo <id> --task-record <n> [--supersedes <n>]...`
|
|
1308
|
+
prints the body of the pull request for the repository's stored change set, and
|
|
1309
|
+
nothing else, then records the `bodySha256` of exactly the bytes it printed as
|
|
1310
|
+
the set's `pullRequest.bodySha256`. Open the pull request from that output.
|
|
1311
|
+
|
|
1312
|
+
```bash
|
|
1313
|
+
launcher-apply-plan body --repo "<id>" --task-record 12 --supersedes 9
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
`<id>` is the repository's id, `owner/name`. It is quoted in the examples because
|
|
1317
|
+
an unquoted `<id>` pasted into a shell is read as a redirection; replace the
|
|
1318
|
+
whole quoted word with the id.
|
|
1319
|
+
|
|
1320
|
+
The approval the body shows is what the hub decides at the time of the run, as
|
|
1321
|
+
`materialize` decides it, from the plan committed at the hub's HEAD: it is never
|
|
1322
|
+
an option or an argument, and a hub whose approval was withdrawn refuses with
|
|
1323
|
+
the same reason `materialize` gives, printing no body. If the hub stored a
|
|
1324
|
+
planned bundle for the set, that bundle must record exactly the same approval,
|
|
1325
|
+
or the run is refused as `binding-mismatch`. Each `--supersedes` is the number
|
|
1326
|
+
of the pull request of an older change set of the repository, once each and not
|
|
1327
|
+
the task record, and needs another change set of that repository in the hub's
|
|
1328
|
+
store (`supersedes-unfounded` otherwise). Every number is digits only.
|
|
1329
|
+
|
|
1330
|
+
A set is bound to one body. Running `body` again with the same arguments prints
|
|
1331
|
+
the same body and changes nothing; a run that would produce another body is
|
|
1332
|
+
refused as `body-bound` and prints nothing, so the hash recorded for the set
|
|
1333
|
+
belongs to one body only; whether the pull request that was opened still carries
|
|
1334
|
+
that body is what `status` checks afterwards. Recording the hash replaces the one
|
|
1335
|
+
stored file of the set atomically, refuses a symbolic link, and changes nothing
|
|
1336
|
+
else in the set: its digest, its file name and every other member stay as they
|
|
1337
|
+
were.
|
|
1338
|
+
|
|
1339
|
+
There is no command that undoes a binding, and no `--dry-run`. A `--task-record`
|
|
1340
|
+
or `--supersedes` number mistyped on a first run binds the set to that body, and
|
|
1341
|
+
every later run with other numbers is refused as `body-bound`; nothing in this
|
|
1342
|
+
package unbinds it. Check the numbers before running `body`.
|
|
1343
|
+
|
|
1344
|
+
Hand the output to the pull request as a file, and open the pull request with
|
|
1345
|
+
`--body-file`, not with `--body "$(launcher-apply-plan body ...)"`: shell command
|
|
1346
|
+
substitution drops the final line feed, and the recorded hash covers it.
|
|
1347
|
+
|
|
1348
|
+
Exit `0` prints the body and only the body. Exit `1` is a refusal and exit `2`
|
|
1349
|
+
is indeterminate or a usage error; each prints nothing on standard output and
|
|
1350
|
+
one line on standard error, `launcher-apply-plan body: refused (<reason>)`,
|
|
1351
|
+
`launcher-apply-plan body: indeterminate (<reason>)` or, for a usage error,
|
|
1352
|
+
`launcher-apply-plan body: usage: <the usage line>`, a fixed reason or the usage
|
|
1353
|
+
line and never an argument, a path or any tool output.
|
|
1354
|
+
|
|
1355
|
+
### Observing the pull request
|
|
1356
|
+
|
|
1357
|
+
`launcher-apply-plan status --repo <id>` reports what the pull request for the
|
|
1358
|
+
repository's stored change set is doing. It runs after the agent has pushed the
|
|
1359
|
+
branch and opened the pull request from the text `renderPullRequest()` returned,
|
|
1360
|
+
and it changes nothing: it asks GitHub three read-only questions (who is
|
|
1361
|
+
asking, which pull requests are open, and where the default branch is) with
|
|
1362
|
+
`gh api --method GET`, reads only commits that are already in the local clone
|
|
1363
|
+
through git, and never fetches a pull request's head, checks anything out or
|
|
1364
|
+
writes a file, an index or any ref but one. It needs a full clone: a partial
|
|
1365
|
+
(promisor) clone, such as a blobless or treeless one, is refused up front as
|
|
1366
|
+
`partial-clone`, before any object is read, because git would fetch what such a
|
|
1367
|
+
clone lacks. All its git calls also run with lazy fetch off, and each has a
|
|
1368
|
+
30 second limit, including those of the preconditions it shares with `verify`
|
|
1369
|
+
except the base-commit reads made by the hub admission, which have no limit. A
|
|
1370
|
+
read of a base blob inside `verify`'s own checks (a base lockfile, for one) that
|
|
1371
|
+
fails or times out is taken as an absent file, so `status` can answer `diverged`
|
|
1372
|
+
where `indeterminate` is the truer answer. The limit reaches those shared
|
|
1373
|
+
reads through the environment variable `CLOSSYS_LAUNCHER_GIT_TIMEOUT_MS`, which
|
|
1374
|
+
`status` sets for the duration of its own checks, one `status` call at a time in
|
|
1375
|
+
a process; set in a shell, the same variable also limits the git calls of
|
|
1376
|
+
`verify` and `materialize`, which set none. The one
|
|
1377
|
+
ref it writes is the one `verify` writes: the fetch of the default branch into
|
|
1378
|
+
its remote-tracking ref (`refs/remotes/origin/<default branch>`), which also
|
|
1379
|
+
leaves `FETCH_HEAD` and any new objects of that branch in the clone. It runs the
|
|
1380
|
+
hub's readiness executable as `verify` does.
|
|
1381
|
+
|
|
1382
|
+
```bash
|
|
1383
|
+
launcher-apply-plan status --repo ./site-checkout
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
It prints one line, `launcher-apply-plan status: <state>`, then a fixed reason
|
|
1387
|
+
in parentheses and `#<n>` for each pull request it is about, and nothing else:
|
|
1388
|
+
never a body, a title, a login, a branch, a path or any tool output.
|
|
1389
|
+
|
|
1390
|
+
| State | Exit | Meaning |
|
|
1391
|
+
| --- | --- | --- |
|
|
1392
|
+
| `proposed` | `0` | An open pull request carries this set's marker, was opened by the person running this, from and into the set's own repository, on the set's branch and title, its body hashes to the `bodySha256` that `body` recorded, and its head commit passes every check `verify` makes, including the ledger's exact bytes. |
|
|
1393
|
+
| `applied` | `0` | The default branch's tip is in the clone and holds every `after` and every key the set writes, however it got there, with no open pull request needed. |
|
|
1394
|
+
| `planned` | `2` | Neither. |
|
|
1395
|
+
| `diverged` | `1` | The pull request that carries this set's marker does not match it: a different base branch, branch or title, a body whose hash is not the recorded `bodySha256` (`body-mismatch`), a head that is not in the clone, or a head that fails a `verify` check. |
|
|
1396
|
+
| `superseded` | `2` | An older change set this hub stored for the repository has an open pull request. It outranks `proposed`, so it is also the state when this set's own pull request is open beside the older one. |
|
|
1397
|
+
| `indeterminate` | `2` | Something could not be read or trusted, with one of the reasons below. |
|
|
1398
|
+
|
|
1399
|
+
When more than one applies, the first of `indeterminate`, `diverged`,
|
|
1400
|
+
`superseded`, `proposed`, `applied` and `planned` wins. A pull request whose
|
|
1401
|
+
body names the marker counts only when its author is the person running this
|
|
1402
|
+
and its head and base are both the set's repository; any other is
|
|
1403
|
+
`foreign-marker`. A body that has a carriage return, a marker that is not the
|
|
1404
|
+
whole of its first line, or a marker `readChangeSetMarker()` cannot read is
|
|
1405
|
+
`marker-malformed`. Any open pull request whose body names the marker word, from
|
|
1406
|
+
anyone, therefore makes `status` `indeterminate` (`foreign-marker`) until it is
|
|
1407
|
+
closed: closing the stray pull request is the remedy.
|
|
1408
|
+
|
|
1409
|
+
Every reason `indeterminate` can carry:
|
|
1410
|
+
|
|
1411
|
+
- The set and the clone: `change-set-absent`, `change-set-invalid`,
|
|
1412
|
+
`repository-invalid`, `missing-clone` and `partial-clone` (a blobless,
|
|
1413
|
+
treeless or other partial clone; use a full clone).
|
|
1414
|
+
- GitHub: `port-failed`, `port-malformed` and `too-many-open`. Only the first
|
|
1415
|
+
page of 100 open pull requests is read, so a listing of 100 or more cannot be
|
|
1416
|
+
shown to be whole and is refused.
|
|
1417
|
+
- The markers: `foreign-marker`, `marker-malformed`, `unknown-digest` (a digest
|
|
1418
|
+
this hub never stored) and `duplicate-digest`.
|
|
1419
|
+
- The body: `body-unbound`, when the set has no `bodySha256` because `body` never
|
|
1420
|
+
ran for it. Such a set is never `proposed` and never `diverged`; run `body`,
|
|
1421
|
+
which records the hash, and open the pull request from its output.
|
|
1422
|
+
- Git, over commits already in the clone: `tip-not-local` (the default branch's
|
|
1423
|
+
tip is not in the clone), `tip-unreadable` (the tip's tree could not be read),
|
|
1424
|
+
`object-unreadable` (a corrupt, missing or unreachable object of the pull
|
|
1425
|
+
request's head, or a git call that timed out on it) and `status-failed` (any
|
|
1426
|
+
other failure, including a git call that timed out, a git that cannot run or
|
|
1427
|
+
an unexpected error; nothing is guessed from it).
|
|
1428
|
+
- The clone, the hub and the admission, which `verify` needs and which are
|
|
1429
|
+
judged before the pull request's own fields: a refusal there, of either exit
|
|
1430
|
+
code of `verify` (for example `remote-tip-mismatch`, when the local default
|
|
1431
|
+
branch is not the remote's), is `indeterminate` here, because it is about the
|
|
1432
|
+
clone and not about the pull request. `verify`'s own reasons for an
|
|
1433
|
+
unreadable tree, such as `status-unreadable`, `symlink-ancestor` and
|
|
1434
|
+
`lockfile-format-unsupported`, appear the same way.
|
|
1435
|
+
|
|
1436
|
+
`proposed` does not check the head's ancestry to the base commit, and nothing in
|
|
1437
|
+
this unit does: it checks the head's tree and the paths that differ from the
|
|
1438
|
+
base, as `verify` does, so a head built on a newer default branch that reverts
|
|
1439
|
+
it can look the same. A reviewer reading the pull request's own diff on GitHub
|
|
1440
|
+
is what would notice.
|
|
1441
|
+
|
|
1442
|
+
`status` does notice a body edited after it was opened. It hashes the body of
|
|
1443
|
+
this set's own pull request, as GitHub returned it, with no trimming and no
|
|
1444
|
+
change to line endings or the final line feed, and `proposed` needs
|
|
1445
|
+
`sha256:` and the hex SHA-256 of its UTF-8 bytes to equal the set's
|
|
1446
|
+
`bodySha256`. An appended line, a missing final line feed, a trailing space or
|
|
1447
|
+
a character that is not well-formed UTF-16 is `diverged` (`body-mismatch`). The
|
|
1448
|
+
body is checked after the base branch, branch and title and before the head, so
|
|
1449
|
+
the first of those that differs is the reason given. An older set's pull request
|
|
1450
|
+
is never hashed, and neither hash nor body is printed.
|
|
1451
|
+
|
|
1452
|
+
`verify` now reads a path the set removes with a `lstat` alone. A file that is
|
|
1453
|
+
still there but that `verify` cannot read used to raise (exit 2, the usage
|
|
1454
|
+
line); it now reports `removal-present` (exit 1), which is the truth about a
|
|
1455
|
+
path that should be gone.
|
|
1456
|
+
|
|
1457
|
+
## Taking the registry snapshot
|
|
1458
|
+
|
|
1459
|
+
`launcher-apply-plan snapshot --request <file> [--out <file>]` takes the
|
|
1460
|
+
registry snapshot a plan's exact packages are resolved from (#1178). It is
|
|
1461
|
+
the only step of applying a plan that reads the package registry. It records what the
|
|
1462
|
+
registry said; it decides nothing from it. Deciding is
|
|
1463
|
+
`advisor-resolve-packages`'s job, in `@clossys/advisor`.
|
|
1464
|
+
|
|
1465
|
+
- **Request.** `<file>` holds the report `advisor-package-request` prints,
|
|
1466
|
+
`{ "state": "satisfied", "names": [...], "findings": [] }`, or just
|
|
1467
|
+
`{ "names": [...] }`, read as strict JSON (invalid UTF-8, a byte order
|
|
1468
|
+
mark or a repeated key is refused). Every name must be a scoped package
|
|
1469
|
+
name in the publishing scope this package was built with, and appear once.
|
|
1470
|
+
A request with another field, another state or any finding is refused
|
|
1471
|
+
before anything is fetched.
|
|
1472
|
+
- **Fetch.** For each name, in name order and one at a time, a `GET` of
|
|
1473
|
+
the package's full registry document at `{registry}/{name}`, with the slash
|
|
1474
|
+
in the name percent-encoded (`@scope%2Fname`), the same encoding
|
|
1475
|
+
`@clossys/integrator` uses. The registry is the one in this repository's
|
|
1476
|
+
`package-scope.json`, packed into this package at build time.
|
|
1477
|
+
- **Transport.** Node's own `fetch`. The only headers this step sets are
|
|
1478
|
+
`accept: application/json` and `accept-encoding: identity`, and never an
|
|
1479
|
+
`Authorization` header; Node's fetch adds its own default, non-credential
|
|
1480
|
+
headers. The step does not run the npm CLI, and reads no
|
|
1481
|
+
`.npmrc` and no token from the environment. If Node is started with an
|
|
1482
|
+
environment proxy (`NODE_USE_ENV_PROXY`), requests go through that proxy.
|
|
1483
|
+
No registry credential is ever sent; a username and password written in
|
|
1484
|
+
the proxy URL itself are sent only to that proxy, as Node's fetch does. A redirect is refused, never followed.
|
|
1485
|
+
`accept-encoding: identity` asks for the body uncompressed, so when the
|
|
1486
|
+
server honours it the size cap and `responseSha256` apply to the exact
|
|
1487
|
+
bytes received. A response body is
|
|
1488
|
+
read as a stream and abandoned as soon as it passes 10 MiB; a declared
|
|
1489
|
+
length over that is refused before any of the body is read. If a server
|
|
1490
|
+
compresses the body anyway, Node's `fetch` decodes it and the 10 MiB cap
|
|
1491
|
+
counts the decoded bytes, so the read is still bounded. Each request, body
|
|
1492
|
+
included, is abandoned after 30 seconds.
|
|
1493
|
+
- **Answers.** A `200` is projected into the snapshot. A `404` is recorded
|
|
1494
|
+
as `status: "not-found"`. Anything else stops the step at that package:
|
|
1495
|
+
a transport error, a timeout, a redirect, any other status, an oversize
|
|
1496
|
+
body, or a `200` body that is not strict JSON or is not that package's
|
|
1497
|
+
registry document. Nothing further is fetched, no snapshot is written, and
|
|
1498
|
+
the exit code is `2`. An earlier snapshot at the output path is left
|
|
1499
|
+
untouched, and must not be used: exit `2` means this run recorded nothing.
|
|
1500
|
+
- **Projection.** Only what the registry snapshot contract declares is
|
|
1501
|
+
kept: the version the `latest` dist-tag names, or `null`, and, when the
|
|
1502
|
+
document lists that version, that one version's integrity value and tarball
|
|
1503
|
+
URL exactly as served, whether it is deprecated, when it was published, and
|
|
1504
|
+
whether it lists attestations. Every other version, dist-tag and field is
|
|
1505
|
+
ignored. The document must name the requested package, and the version's
|
|
1506
|
+
own entry must carry the version number `latest` names; otherwise nothing
|
|
1507
|
+
is written. `responseSha256` is the SHA-256 of the response body's bytes
|
|
1508
|
+
as received; if a server compressed the body despite
|
|
1509
|
+
`accept-encoding: identity`, it is the SHA-256 of the decoded body.
|
|
1510
|
+
- **Output.** The snapshot is written only after the exact text to be
|
|
1511
|
+
written has been read back strictly and has passed
|
|
1512
|
+
`docs/contracts/registry-snapshot.json`
|
|
1513
|
+
(in the public repository, not shipped in this package; its content is
|
|
1514
|
+
packed at build time), schema and code rules N1-N3 both. It goes to
|
|
1515
|
+
`--out`, by default `clossys/.state/apply/registry-snapshot.json` under the
|
|
1516
|
+
current directory, which should be the hub. It is two-space JSON with a
|
|
1517
|
+
final newline, with packages sorted by name, so the same registry answers
|
|
1518
|
+
give the same bytes apart from `fetchedAt`. `fetchedBy` is this package's
|
|
1519
|
+
own name and version. The write is atomic: a temporary file in the same
|
|
1520
|
+
directory is written, flushed to disk and renamed over the target, so a
|
|
1521
|
+
reader sees the old file or the whole new one.
|
|
1522
|
+
|
|
1523
|
+
A message names a package by its position in the request, `names[<n>]`,
|
|
1524
|
+
never by its name, and never quotes a response or the request: a name is
|
|
1525
|
+
request text, so the caller looks position `<n>` up in the request file it
|
|
1526
|
+
wrote. Exit codes: `0` means the snapshot was written, or that `--help` printed
|
|
1527
|
+
the usage;
|
|
1528
|
+
`2` means nothing was written, whether because of a usage error, an
|
|
1529
|
+
unreadable or refused request, or a registry answer this step cannot record.
|
|
1530
|
+
A snapshot is a record of what the registry answered, not evidence of where
|
|
1531
|
+
a package came from; that is shown by verifying the package's provenance,
|
|
1532
|
+
which this step does not do.
|
|
1533
|
+
|
|
1534
|
+
## Checking provenance (V9)
|
|
1535
|
+
|
|
1536
|
+
`checkSetProvenance({ tree, hubRoot, items }, ports?)`
|
|
1537
|
+
returns, for one change set, its V9 `ApplyCheck` entries
|
|
1538
|
+
(`Promise<readonly ApplyCheck[]>`). `plan` runs it on its temporary tree,
|
|
1539
|
+
after the lockfile step passes; `materialize` and `verify` do not yet run it.
|
|
1540
|
+
|
|
1541
|
+
- **Engine.** It runs the hub's own `node_modules/.bin/integrator-provenance-check --cwd <tree>`
|
|
1542
|
+
(`PROVENANCE_CHECK_BIN`), never through `npx` and never looked up on `PATH`. If
|
|
1543
|
+
that bin is missing, or its real path is not inside the hub's installed
|
|
1544
|
+
`@clossys/integrator`, the result is indeterminate (`engine-missing-bin`).
|
|
1545
|
+
The child gets an environment built from a fixed list of variables, so no
|
|
1546
|
+
parent credential, proxy or CA-trust variable reaches it, and its time and
|
|
1547
|
+
output are capped (`PROVENANCE_CHECK_TIMEOUT_MS`, `PROVENANCE_CHECK_MAX_BUFFER`).
|
|
1548
|
+
The JSON report is parsed strictly; exit `2`, unreadable output, or output
|
|
1549
|
+
that contradicts the exit code is indeterminate.
|
|
1550
|
+
- **What gates.** Every `install` and `pin-starter` item except one whose
|
|
1551
|
+
`satisfiedInBase` is exactly `true`. Each must be `verified` at exactly its version. One
|
|
1552
|
+
missing from the report is indeterminate; a verified version other than the
|
|
1553
|
+
act's is violated (`version-mismatch`). Other `@clossys/*` packages in the
|
|
1554
|
+
report never gate, so an unrelated violated legacy pin passes.
|
|
1555
|
+
- **No exception.** An unverified package is never satisfied. One the bin
|
|
1556
|
+
reports `violated` stays violated (`provenance-unverified`), and one it
|
|
1557
|
+
cannot decide stays indeterminate. The first-publication exception the
|
|
1558
|
+
design allows for (D20) is deliberately not implemented: a registry
|
|
1559
|
+
snapshot records only the one version `latest` names, so it cannot show
|
|
1560
|
+
that a version is a package's first publication, and Integrator reports a
|
|
1561
|
+
failed attestation the same way as a missing one. Any exception built on
|
|
1562
|
+
that would also pass a package with earlier releases whose latest release
|
|
1563
|
+
fails verification. Until the snapshot contract records evidence of a first
|
|
1564
|
+
publication, an unattested first publication blocks the apply (fail
|
|
1565
|
+
closed).
|
|
1566
|
+
- **Verdict.** Indeterminate outranks violated: when any indeterminate rule
|
|
1567
|
+
applies, only indeterminate entries are returned. A set with nothing to gate
|
|
1568
|
+
returns one satisfied entry with no rule, without running the engine.
|
|
1569
|
+
- **Soundness boundary.** A pass means every version the set installs or
|
|
1570
|
+
updates is verified by the hub's pinned Integrator at check time, with no
|
|
1571
|
+
exception. It does not cover transitive dependencies or a later republish.
|
|
1572
|
+
The bin check accepts any regular file inside the installed
|
|
1573
|
+
`@clossys/integrator` package and does not compare the installed version
|
|
1574
|
+
with the hub's pin; the hub's `node_modules` is trusted.
|
|
1575
|
+
|
|
1576
|
+
`registrySnapshotDigest(snapshot)` returns a registry snapshot's contract digest
|
|
1577
|
+
(`sha256:` and 64 lowercase hexadecimal digits) from the snapshot's contents,
|
|
1578
|
+
independent of fetch time and of the order of packages and versions; it throws a
|
|
1579
|
+
`TypeError` for a snapshot that does not validate. The provenance check does not
|
|
1580
|
+
read a snapshot; the digest is for the plan binding a later change wires in.
|
|
1581
|
+
|
|
1582
|
+
Types: `ProvenanceGateInput`, `ProvenanceGatePorts`.
|
|
320
1583
|
|
|
321
1584
|
## Why this is not Advisor, Starter, Builder, installer, creator, or a connector
|
|
322
1585
|
|
|
@@ -351,11 +1614,16 @@ intact.
|
|
|
351
1614
|
|
|
352
1615
|
## Requirements
|
|
353
1616
|
|
|
354
|
-
Node.js 20+, ESM, GitHub `gh`, and no runtime dependencies.
|
|
355
|
-
|
|
356
|
-
|
|
1617
|
+
Node.js 20+, ESM, GitHub `gh`, and no runtime dependencies. The registry
|
|
1618
|
+
snapshot step needs HTTPS access to the public registry, and no credential.
|
|
1619
|
+
Creating a new hub needs permission to create a private repository under the
|
|
1620
|
+
inferred owner. Appointing uses the current checkout and does not create a second
|
|
357
1621
|
repository.
|
|
358
1622
|
|
|
359
1623
|
## Licence
|
|
360
1624
|
|
|
361
1625
|
MIT.
|
|
1626
|
+
|
|
1627
|
+
## Changelog
|
|
1628
|
+
|
|
1629
|
+
Release notes for every version are in the [changelog](https://github.com/clossys/foundry/blob/main/docs/changelogs/launcher.md), kept in the public repository rather than in the installed package.
|