@sous-io/sous 1.0.13 → 1.0.14

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.
@@ -52,3 +52,4 @@
52
52
  - [0020: Config expressions](adrs/0020-config-expressions.md)
53
53
  - [0021: Reporting as levels and styles](adrs/0021-reporting-levels-and-styles.md)
54
54
  - [0022: Intents return outputs](adrs/0022-intents-return-outputs.md)
55
+ - [0023: The project's lockfile is the only lock](adrs/0023-the-project-lockfile-is-the-only-lock.md)
@@ -132,7 +132,7 @@ A subscriber is offered the missing plugin when they subscribe; see
132
132
  ## Declare the variables a recipe needs
133
133
 
134
134
  A recipe that needs a value from the project asks through a **definition**: a published specification, never a
135
- value. Sous asks the question only when a subscribed recipe needs the variable and no valid answer is in scope.
135
+ value. Sous asks the question only when a recipe the project holds needs the variable and no valid answer is in scope.
136
136
 
137
137
  ```yaml
138
138
  variables:
@@ -177,7 +177,7 @@ variables:
177
177
  a value the environment already carries, such as `GITHUB_TOKEN`; `x-intentional: true` then silences the
178
178
  release warning about claiming a well-known name (`PATH`, `HOME`, `USER`, `SHELL`, `GITHUB_TOKEN`,
179
179
  `GITLAB_TOKEN`, `NPM_TOKEN`, or anything starting `AWS_` or `SOUS_`, except the `SOUS_VAR_` names sous
180
- derives itself). Two definitions of the same name may share one variable; two of **different** names
180
+ derives itself). Two definitions of the same name share one variable; two of **different** names
181
181
  claiming the same one is an error.
182
182
  - **`secret: true`** hides the value everywhere sous prints it, and **`scope`** picks the file the answer is
183
183
  written to: `shared` for the committed `.sous/.env`, `local` for the gitignored `.sous/.env.local`. A secret
@@ -187,19 +187,23 @@ variables:
187
187
  answering, and the message names your pattern. Within a major version a schema may only LOOSEN: tightening a
188
188
  constraint is a major bump, and an upgrade re-validates stored answers, re-prompting only where an old one no
189
189
  longer fits.
190
+ - **Reading another recipe's variable** needs no definition: list that recipe under `depends` and use the name in
191
+ your templates. Redefining it makes a second definition, which sous combines with the first; see
192
+ [Two recipes, one name](repositories-variables.md#two-recipes-one-name).
190
193
  - **Further reading:** [Recipe variables](repositories-variables.md) covers how an answer is found at build
191
194
  time; [Variable definitions](repositories-file-formats.md#variable-definitions) is the full field table.
192
195
 
193
196
  ## Declare dependencies and co-subscriptions
194
197
 
195
198
  `depends` fetches and pins a recipe whose files stay OUT of a subscriber's output but are addressable from your
196
- own templates, and from the templates of any project whose lockfile pins it. `subscribes` is a co-subscription: its questions run and its files DO enter the output, which is
197
- how a curated bundle is built. Both hold plain strings naming their target by where it lives.
199
+ own templates, and from the templates of any project whose lockfile pins it. `subscribes` is a co-subscription: its
200
+ files DO enter the output, which is how a curated bundle is built. Under either list, the other recipe's questions
201
+ are asked and its answers reach your templates. Both hold plain strings naming their target by where it lives.
198
202
 
199
203
  ```yaml
200
204
  depends:
201
- - workflow/qa-helper # a sibling, released alongside me
202
- - workflow/qa-helper@^1.1 # a sibling with a range; uncommon
205
+ - workflow/qa-helper # a sibling: ^ its version at release
206
+ - workflow/qa-helper@1.1.4 # a sibling at exactly one version
203
207
  - github://sous-io/sous-recipes/workflow/sat@^1.0 # another repository
204
208
  - gitlab://gitlab.example.com/group/subgroup/proj/-/qa/lint # a self-hosted host, with subgroups
205
209
  - https://github.com/acme/recipes/tree/main/recipes/lint # a URL copied from the browser
@@ -207,14 +211,15 @@ subscribes:
207
211
  - quality/code-review # co-subscribed: its files land too
208
212
  ```
209
213
 
210
- A **bare ref** is a sibling in this repository; with no range it means "the version released alongside me", since
211
- one run cuts both tags and records the exact version in the index. A location names another repository: a
214
+ A **bare ref** is a sibling in this repository; with no range it means `^<the sibling's version at release>`, any
215
+ later version with the same major. A location names another repository: a
212
216
  locator, an HTTPS or SSH URL, or a URL copied from the browser. A ref is stored and printed as its published
213
217
  **identity** (namespace and recipe), never as a folder path. Any spelling that settles to one identity is
214
218
  accepted, including a folder path or a pasted browser URL: a recipe stored at `recipes/shared/sat/` and
215
219
  published as `workflow/sat` may be written `github://owner/repo/workflow/sat` or
216
220
  `https://github.com/owner/repo/tree/main/recipes/shared/sat`, and the release settles the second through the
217
- other repository's index. The optional range after `@` follows npm's rules. `local://` locators and `repo:`
221
+ other repository's index. The optional range after `@` follows npm's rules, and may be exact when your recipe needs
222
+ certainty; nothing else pins a dependency to one version except the subscribing project's lockfile. `local://` locators and `repo:`
218
223
  short names are refused: a local repository is a consumer's convenience, and a short name is one project's
219
224
  private label. See [Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location)
220
225
  and every accepted spelling in [Ref forms](repositories-file-formats.md#ref-forms).
@@ -292,8 +297,9 @@ this release's scope** is fine: the release depends on the last published versio
292
297
  release's scope; 'workflow/qa-variables@0.1.1' will depend on 'workflow/qa-helper@0.1.0'.
293
298
  ```
294
299
 
295
- Each version's resolved dependencies are written into the index, so a consumer installing that version gets what
296
- it was published with rather than ranges re-resolved months later. Each dependency also records the manifest
300
+ Each version's dependencies are written into the index as ranges: the range the manifest declares, or
301
+ `^<the sibling's version at release>` for a sibling named with no range. A consumer resolves those ranges, and
302
+ its own lockfile pins what it got; that lockfile is the only lock. Each dependency also records the manifest
297
303
  entry that brought it in and whether it is a co-subscription or a build dependency, and each version records the
298
304
  variable definitions its manifest publishes, so `sous recipe show` and `sous subscription add --dry-run` can say
299
305
  what a recipe installs and asks before anything is fetched. A published version never changes: its content
@@ -67,8 +67,8 @@ upstream's newest version beside the installed one.
67
67
 
68
68
  Read `sous recipe show` before subscribing. It works offline, from the index alone: every published
69
69
  version, what the version depends on (the manifest entry that brings each dependency in, whether it
70
- is a co-subscription whose files land here or a build dependency, and the version it was released
71
- against), every recipe a subscription would install, and every question that would ask. Only the
70
+ is a co-subscription whose files land here or a build dependency, and the range of versions it
71
+ may take), every recipe a subscription would install, and every question that would ask. Only the
72
72
  files it publishes wait until it is installed. A preset set, a recipe that only bundles others,
73
73
  reads like this:
74
74
 
@@ -77,11 +77,11 @@ $ sous recipe show omakase/house
77
77
  Latest version : 1.0.0
78
78
  Pinned version : this project pins none
79
79
  Subscribed : no
80
- Dependency Declared as Resolved to Repository Kind
81
- --------------- ------------------------------ ----------- --------------- ----------------
82
- tools/lint tools/lint 1.0.0 this repository build dependency
83
- workflow/notes the whole 'workflow' namespace 1.0.0 this repository co-subscription
84
- workflow/review the whole 'workflow' namespace 1.0.0 this repository co-subscription
80
+ Dependency Declared as Resolved to Repository Kind
81
+ --------------- ------------------------------ ---------------- --------------- ----------------
82
+ tools/lint tools/lint the range ^1.0.0 this repository build dependency
83
+ workflow/notes the whole 'workflow' namespace the range ^1.0.0 this repository co-subscription
84
+ workflow/review the whole 'workflow' namespace the range ^1.0.0 this repository co-subscription
85
85
  ➔ What subscribing asks you:
86
86
  These recipes ask 3 questions, 3 of which nothing answers yet.
87
87
  workflow/notes asks 1 question:
@@ -168,7 +168,8 @@ change when you say yes: `.sous/conf.d/510-subscriptions.jsonc` records the subs
168
168
  the new skills are on disk when it returns; `--no-build` defers that, and a failed build keeps it.
169
169
 
170
170
  ?> Only a recipe a manifest lists under `subscribes` contributes files; one listed under `depends`,
171
- like `workflow/qa-helper` above, is fetched and pinned but stays out of your output. See
171
+ like `workflow/qa-helper` above, is fetched and pinned but stays out of your output. Recipes under both
172
+ lists have their questions asked. See
172
173
  [Dependencies](repositories.md#dependencies).
173
174
 
174
175
  ## Choose where the files land
@@ -312,6 +313,11 @@ What it will and will not do:
312
313
  - A pin moves only within the range its subscription declares, or the range the recipe depending
313
314
  on it declares; a range is never widened. Prereleases count only for a subscription that opted
314
315
  into them.
316
+ - Every subscription in scope resolves together, so a recipe several of them need takes one version
317
+ that every holder's range allows: the pinned one while it still fits, otherwise the newest that
318
+ does. When no version fits every holder, the subscription that cannot join is refused, naming each
319
+ holder and its range. A library held only through `depends` that its holders need at
320
+ incompatible versions is instead pinned once per holder.
315
321
  - Dependencies move with the closure: a dependency a newer version adds is pinned, and one it
316
322
  dropped leaves the lockfile unless something else still holds it.
317
323
  - Only the lockfile changes. Your subscriptions stay exactly as they are written.
@@ -217,7 +217,7 @@ fetches only this file; nothing more is downloaded until a project subscribes.
217
217
  "recipes": { "quality/qa-remote-dep": { "path": "recipes/quality/qa-remote-dep",
218
218
  "versions": { "0.1.0": {
219
219
  "dependencies": {
220
- "workflow/qa-helper": { "version": "0.1.0", "declared": "workflow", "kind": "subscribes" },
220
+ "workflow/qa-helper": { "range": "^0.1.0", "declared": "workflow", "kind": "subscribes" },
221
221
  "workflow/sub-agent-delegation": { "range": "^1.0", "repo": "github.com/sous-io/sous-recipes",
222
222
  "declared": "github://sous-io/sous-recipes/workflow/sub-agent-delegation@^1.0",
223
223
  "kind": "depends" } },
@@ -239,20 +239,26 @@ fetches only this file; nothing more is downloaded until a project subscribes.
239
239
  | `recipes` | top level | Keyed `namespace/recipe`; a recipe whose namespace is not declared is an error. Each entry carries `path` (the recipe folder), `description`, and `versions`, keyed by exact version |
240
240
  | `hash`, `tag` | per version | The content hash verified after every fetch, and the tag, which must be exactly `namespace/recipe@version` so a version can never point at a branch |
241
241
  | `prerelease`, `releasedAt`, `seeded` | per version | Whether ranges skip it unless a subscription opts in, when it was released, and whether it is the copy sous folds in from its own package |
242
- | `dependencies` | per version | What the version was released against, keyed `namespace/recipe`; see below |
242
+ | `dependencies` | per version | The range each dependency may take, keyed `namespace/recipe`; see below |
243
243
  | `variables` | per version | The version's variable definitions, exactly as its manifest publishes them (see [Variable definitions](#variable-definitions)), with `required`, `secret` and `scope` filled in; `[]` when it asks nothing |
244
244
  | `depends`, `subscribes` | per version | The version's manifest lists, exactly as written; `[]` when empty |
245
245
 
246
- Dependencies are resolved at release time and keyed `namespace/recipe`. An entry carries `version` (a
247
- sibling, resolved exactly) or `range` plus `repo` (the identity of the repository publishing it, whose own
248
- index resolves the range); at least one is required. Installing a version installs these rather than
249
- re-resolving the manifest's ranges.
246
+ Dependencies are recorded at release time and keyed `namespace/recipe`. Each entry carries `range`: the range
247
+ the manifest declares, or `^<the sibling's version at release>` for a sibling named with no range (`*` when the
248
+ sibling has no version yet). A dependency in another repository also carries `repo`, the identity of the
249
+ repository publishing it, whose own index resolves the range. The index never pins a dependency to one version;
250
+ the consuming project resolves every range and its lockfile is the only lock.
251
+
252
+ An entry written by an older release carries `version`, the exact version that release resolved, instead of
253
+ `range`. Sous reads it as `^<version>`, further limited by the range in the entry's `declared` field when it
254
+ holds one, so `{ "version": "1.1.1", "declared": "workflow/sub-agent-delegation@~1.1" }` allows any `1.1.x`
255
+ from `1.1.1` up. Nothing is republished to change it.
250
256
 
251
257
  **Each version describes itself.** So that a recipe can be described before anything is fetched, every
252
258
  version a release records carries more than its dependencies. Each dependency records `declared`, the
253
259
  manifest entry that brings it in exactly as written (a recipe such as `workflow/qa-helper@^1.0`, a whole
254
260
  namespace such as `workflow`, or a locator), and `kind`: `subscribes` for a co-subscription, whose files
255
- land and whose questions are asked, or `depends` for a build dependency. When several entries cover one
261
+ land, or `depends` for a build dependency, whose files stay out; the questions of both are asked. When several entries cover one
256
262
  recipe, an entry naming it wins over a namespace, and it is a co-subscription when any entry covering it is
257
263
  one. The version records `variables`, its manifest's definitions, and `depends` and `subscribes`, its
258
264
  manifest's lists as written; the lists are what a consumer walks, because a namespace of another repository
@@ -263,15 +269,16 @@ the resolver walks a recipe whose files are not on the machine from its index en
263
269
  its manifest. A version recorded before a release wrote these fields keeps its entry as it is; its questions
264
270
  are known only once its files are fetched, and the dry run names it as such.
265
271
 
266
- **A published version's entry is frozen.** Its dependencies are resolved once, when the version is first
267
- recorded, and every later release carries the entry forward exactly as published, like its hash; releasing
272
+ **A published version's entry is frozen.** Its dependencies are recorded once, when the version is first
273
+ published, and every later release carries the entry forward exactly as published, like its hash; releasing
268
274
  other recipes, adding a recipe to a namespace it subscribes to, or raising a sibling it names changes nothing
269
275
  in it. A release still checks the recorded list against the version's manifest: a recipe the manifest names
270
- that the list lacks, a recipe in the list the manifest does not declare, a sibling outside its declared
271
- range, or another repository's entry with a different repository or range is an error naming the recipe, the
272
- version and each difference; restore the entry if the index was edited, or bump the version to publish
273
- different dependencies. A version rebuilt from its tag, when the index lost it, has its dependencies resolved
274
- against the repository as it stood at that tag, then frozen the same way. An entry written before sous
276
+ that the list lacks, a recipe in the list the manifest does not declare, a recorded range that is not the
277
+ declared one (for a sibling named with no range, not a `^<version>` range or `*`), an older entry's `version`
278
+ outside its declared range, or another repository's entry with a different repository is an error naming the
279
+ recipe, the version and each difference; restore the entry if the index was edited, or bump the version to publish
280
+ different dependencies. A version rebuilt from its tag, when the index lost it, has its dependencies recorded
281
+ from the repository as it stood at that tag, then frozen the same way. An entry written before sous
275
282
  recorded dependencies carries none, stays that way, and installs by resolving its manifest's ranges.
276
283
 
277
284
  **Fields this sous does not know are kept, not refused.** A later sous may add fields to the index, at any
@@ -311,12 +318,29 @@ Machine-written, committed, and the reason a fresh clone restores with no prompt
311
318
  | `repos.<name>.url`, `.identity`, `.indexHash` | Where the repository lives, as recorded when it was added; the canonical identity the store and index cache file it under; and the hash of the index this lock was resolved against, when known |
312
319
  | `recipes.<key>.repo`, `.version`, `.hash` | The project's short name for the repository, which must appear under `repos`; the version resolved; and the hash verified against the store after every fetch |
313
320
  | `recipes.<key>.requestedBy`, `.kind` | Who holds the entry, the literal `project` or the key of a recipe that requires it, and whether that holder is a `subscribes` or a `depends` |
321
+ | `recipes.<key>.pins.<holder>.version`, `.hash` | A version pinned for one holder alone, keyed by that holder's recipe key; absent when every holder uses `version` |
314
322
 
315
323
  A repository appears under two names on purpose: the key is the project's own short name, which every
316
324
  message uses, while `identity` is what the store is keyed by, so two projects with different labels share
317
325
  one cached copy. `identity` is optional on read only, for older lockfiles, and is derived from the URL on
318
326
  the next write. `requestedBy` makes removal safe; an entry goes when its last holder does.
319
327
 
328
+ `pins` appears only when a holder that reaches the recipe through `depends` needs a version no other
329
+ holder's range allows. The entry's `version` is the one the project uses (a recipe some holder reaches
330
+ through `subscribes` has exactly one); each holder under `pins` uses its own version instead, for its
331
+ includes and its questions, and nothing from that version reaches the project's files. Every holder under
332
+ `pins` is also listed in `requestedBy`, and loses its pin when it stops holding the entry. Each pinned
333
+ version is fetched, checked against its hash and kept in the store like the shared one.
334
+
335
+ ```json
336
+ "support/base": { "kind": "depends", "repo": "fixtures", "version": "1.0.0", "hash": "sha256-1b...",
337
+ "requestedBy": ["lib/one", "lib/two"],
338
+ "pins": { "lib/two": { "version": "2.0.0", "hash": "sha256-9c..." } } }
339
+ ```
340
+
341
+ A lockfile with no `pins` reads exactly as it did before. A version of sous older than this field refuses a
342
+ lockfile that carries one, naming the unknown key, because the format is strict.
343
+
320
344
  ## The store on disk
321
345
 
322
346
  The store is machine-wide, immutable per version, and disposable (everything is re-fetchable from a
@@ -93,8 +93,8 @@ sous recipe show workflow/task-files
93
93
  ```
94
94
 
95
95
  On a machine that has never fetched the recipe, sous shows what the index knows: every published
96
- version, and every dependency with each declared range beside the exact version the index resolved
97
- it to. The questions it asks are not known until it is fetched; run the same command after
96
+ version, and every dependency with the range it was declared with and the range the index records
97
+ for it. The questions it asks are not known until it is fetched; run the same command after
98
98
  subscribing and it adds a table of the variables the recipe publishes.
99
99
 
100
100
  ## 4. Subscribe to a recipe
@@ -31,6 +31,37 @@ match on a worker under a fixed budget of 100 milliseconds, and a pattern that e
31
31
  naming the pattern and the recipe rather than blaming your answer. See
32
32
  [A variable pattern that runs out of time](repositories-troubleshooting.md#a-variable-pattern-that-runs-out-of-time).
33
33
 
34
+ ## Two recipes, one name
35
+
36
+ Answers are shared by name, so when several recipes, or several versions of one recipe, define a variable of
37
+ the same name, sous asks **one** question, built from the strictest combination of every definition:
38
+
39
+ - secret when any definition is secret, `scope: local` when any is local, and required when any is required;
40
+ - only the values every definition accepts: the `enum` options they share, the highest `min` and `minLength`,
41
+ the lowest `max` and `maxLength`, and every `pattern`;
42
+ - the first definition's prompt, description and example, with the question naming the other recipes
43
+ (`workflow/notes, also used by review/checklist`);
44
+ - the first `default` that passes the combination; a default that fails it is dropped.
45
+
46
+ For example, one recipe defines `port` as a number of at least 1000 and another as a secret number of at most
47
+ 9000: you are asked once, for a secret number from 1000 to 9000, stored locally.
48
+
49
+ When no answer could satisfy every definition (one says `number` and the other `string`, their `enum` lists
50
+ share no option, or one's `min` is above another's `max`), the definitions are not combined. Sous says why,
51
+ naming both, and asks each recipe separately, storing each answer under that recipe's
52
+ [recipe-scoped name](#how-sous-finds-an-answer-the-ladder) (`SOUS_VAR_<NAMESPACE>_<RECIPE>_<VARIABLE>`).
53
+
54
+ Two versions of one recipe (a recipe pinned at a different version for some of the recipes that depend on it)
55
+ would share that recipe-scoped name, so when their definitions cannot be combined, sous asks only one of them:
56
+ the version the lockfile shares, or else the first version the lockfile lists. The other version gets no
57
+ answer, not even its default, and the variable renders empty where that version is used. `sous vars ask`,
58
+ `sous vars list` and `sous vars show`, a build, and the question preview of `sous subscription add --dry-run`
59
+ and `sous recipe show` warn about it, naming both versions, the recipes that use each, and why the definitions
60
+ cannot be combined.
61
+
62
+ A recipe that only reads another recipe's variable, by listing that recipe under `depends` and using the
63
+ name in its templates, does not define it, so it adds nothing to the combination.
64
+
34
65
  ## Answer the questions
35
66
 
36
67
  Subscribing asks whatever is unanswered, so in normal use you rarely run `sous vars ask` yourself; reach for
@@ -93,8 +93,8 @@ with the project.
93
93
  A recipe manifest can declare two relationships to other recipes, and they differ in exactly one
94
94
  respect: whose files end up in your project. `depends` fetches, pins and trust-gates the target
95
95
  and makes it addressable while rendering, but keeps its files out of your output; `subscribes`
96
- does all of that and lands the target's files in your project too, along with the variable
97
- questions it publishes (see [Recipe variables](repositories-variables.md)).
96
+ does all of that and lands the target's files in your project too. Under either list, the variable
97
+ questions the target publishes are asked (see [Recipe variables](repositories-variables.md)).
98
98
 
99
99
  ```yaml
100
100
  depends:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Compiles AI coding agent configuration (CLAUDE.md, skills, memories) from LiquidJS templates",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -39,44 +39,44 @@
39
39
  "sous.config.schema.json"
40
40
  ],
41
41
  "dependencies": {
42
- "@sous-io/plugin-api": "1.0.13"
42
+ "@sous-io/plugin-api": "1.0.14"
43
43
  },
44
44
  "devDependencies": {
45
45
  "esbuild": "^0.27.4",
46
46
  "jsonc-parser": "^3.3.1",
47
47
  "yaml": "^2.9.0",
48
48
  "zod": "^4.4.3",
49
- "@sous-io/documents": "1.0.13",
50
- "@sous-io/plugin-authoring": "1.0.13",
51
- "@sous-io/plugin-build": "1.0.13",
52
- "@sous-io/plugin-claude-code": "1.0.13",
53
- "@sous-io/core": "1.0.13",
54
- "@sous-io/plugin-cli": "1.0.13",
55
- "@sous-io/plugin-confd": "1.0.13",
56
- "@sous-io/plugin-config-files": "1.0.13",
57
- "@sous-io/plugin-formats": "1.0.13",
58
- "@sous-io/plugin-github": "1.0.13",
59
- "@sous-io/plugin-expressions": "1.0.13",
60
- "@sous-io/plugin-git": "1.0.13",
61
- "@sous-io/plugin-harnesses": "1.0.13",
62
- "@sous-io/plugin-gitlab": "1.0.13",
63
- "@sous-io/plugin-init": "1.0.13",
64
- "@sous-io/plugin-compiler": "1.0.13",
65
- "@sous-io/plugin-inspect": "1.0.13",
66
- "@sous-io/plugin-host": "1.0.13",
67
- "@sous-io/plugin-liquid": "1.0.13",
68
- "@sous-io/plugin-local": "1.0.13",
69
- "@sous-io/plugin-mcp": "1.0.13",
70
- "@sous-io/plugin-memories": "1.0.13",
71
- "@sous-io/plugin-npm": "1.0.13",
72
- "@sous-io/plugin-pnpm": "1.0.13",
73
- "@sous-io/plugin-recipe-answers": "1.0.13",
74
- "@sous-io/plugin-recipes": "1.0.13",
75
- "@sous-io/plugin-skills": "1.0.13",
76
- "@sous-io/plugin-variables": "1.0.13",
77
- "@sous-io/plugin-update": "1.0.13",
78
- "@sous-io/plugin-yarn": "1.0.13",
79
- "@sous-io/plugin-refs": "1.0.13"
49
+ "@sous-io/core": "1.0.14",
50
+ "@sous-io/documents": "1.0.14",
51
+ "@sous-io/plugin-build": "1.0.14",
52
+ "@sous-io/plugin-claude-code": "1.0.14",
53
+ "@sous-io/plugin-authoring": "1.0.14",
54
+ "@sous-io/plugin-cli": "1.0.14",
55
+ "@sous-io/plugin-compiler": "1.0.14",
56
+ "@sous-io/plugin-config-files": "1.0.14",
57
+ "@sous-io/plugin-formats": "1.0.14",
58
+ "@sous-io/plugin-expressions": "1.0.14",
59
+ "@sous-io/plugin-confd": "1.0.14",
60
+ "@sous-io/plugin-git": "1.0.14",
61
+ "@sous-io/plugin-github": "1.0.14",
62
+ "@sous-io/plugin-harnesses": "1.0.14",
63
+ "@sous-io/plugin-init": "1.0.14",
64
+ "@sous-io/plugin-host": "1.0.14",
65
+ "@sous-io/plugin-inspect": "1.0.14",
66
+ "@sous-io/plugin-gitlab": "1.0.14",
67
+ "@sous-io/plugin-local": "1.0.14",
68
+ "@sous-io/plugin-liquid": "1.0.14",
69
+ "@sous-io/plugin-mcp": "1.0.14",
70
+ "@sous-io/plugin-memories": "1.0.14",
71
+ "@sous-io/plugin-pnpm": "1.0.14",
72
+ "@sous-io/plugin-recipe-answers": "1.0.14",
73
+ "@sous-io/plugin-npm": "1.0.14",
74
+ "@sous-io/plugin-recipes": "1.0.14",
75
+ "@sous-io/plugin-skills": "1.0.14",
76
+ "@sous-io/plugin-update": "1.0.14",
77
+ "@sous-io/plugin-variables": "1.0.14",
78
+ "@sous-io/plugin-refs": "1.0.14",
79
+ "@sous-io/plugin-yarn": "1.0.14"
80
80
  },
81
81
  "engines": {
82
82
  "node": ">=22"
@@ -134,8 +134,13 @@ The fields in that example, and the ones it leaves out:
134
134
  answers already stored.
135
135
 
136
136
  A template may also read a variable another recipe defines. List that recipe under `depends`, so
137
- the variable is there wherever this recipe is, and branch on it in the template rather than telling
138
- the agent to look the setting up; `about-liquid-templates` shows how.
137
+ the variable is there wherever this recipe is, and MUST NOT redefine it; branch on it in the
138
+ template rather than telling the agent to look the setting up (`about-liquid-templates` shows how).
139
+
140
+ When two recipes do define one name, the project is asked one question built from the strictest
141
+ combination: secret, `local` or required if any definition is, and only the values every definition
142
+ accepts. When no value can satisfy both (different types, no shared `enum` option), each recipe is
143
+ asked separately under its recipe-scoped name.
139
144
 
140
145
  ## Depends and Subscribes
141
146
 
@@ -144,14 +149,18 @@ Both lists name other recipes, and they do different things:
144
149
  - `depends` makes the other recipe a **library** for this one. It is fetched and pinned, and its
145
150
  files can be included from this recipe's templates, but its files never reach the subscriber's
146
151
  output.
147
- - `subscribes` is a **co-subscription**. The other recipe's files land and its questions are asked,
148
- as if the project had subscribed to it too.
152
+ - `subscribes` is a **co-subscription**. The other recipe's files land, as if the project had
153
+ subscribed to it too.
154
+
155
+ Under either list, the other recipe's questions are asked, and its answers are in this recipe's
156
+ templates.
149
157
 
150
158
  Each entry names its target by where it lives:
151
159
 
152
160
  - a bare `namespace/recipe` (or `namespace` for all of it) for a sibling in the same repository.
153
- With no range it means the version released alongside this one; an `@` range (npm's rules) is
154
- allowed but uncommon.
161
+ With no range it means `^<the sibling's version at release>`, the same major version. An `@`
162
+ range follows npm's rules and MAY be exact (`workflow/qa-helper@1.2.0`) when this recipe needs
163
+ certainty; the subscribing project's lockfile is the only thing that pins an exact version.
155
164
  - a location for a recipe in another repository, such as
156
165
  `github://example-org/team-recipes/review/wording@^1.0`, an HTTPS or SSH URL, or a URL copied
157
166
  from the browser. A ref is stored and printed as its published identity (namespace and recipe),
@@ -65,8 +65,9 @@ contribute: https://github.com/harbor-labs/team-recipes/blob/main/CONTRIBUTING.m
65
65
  A recipe's version lives in its manifest, which is the source of truth. A version is **published**
66
66
  once a git tag shaped `namespace/recipe@version` exists for it (for example
67
67
  `review/checklist@1.2.0`). The index records each published version with its content hash, its
68
- tag, the exact versions of its dependencies at release time (each with the manifest entry that
69
- brought it in and whether it is a co-subscription or a build dependency) and the variable
68
+ tag, the range each of its dependencies may take (the declared range, or `^<version at release>`
69
+ for a sibling named with no range; each with the manifest entry that brought it in and whether it
70
+ is a co-subscription or a build dependency) and the variable
70
71
  definitions it publishes, which is how a project lists what a recipe installs and asks before
71
72
  fetching it; for the length of one release commit it also records the version each manifest
72
73
  declares, whose tag that release cuts next. A published version never changes: new files need a
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 1.0.13
14
+ version: 1.0.14
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous