@sous-io/sous 0.2.0 → 0.2.2

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.
@@ -1,18 +1,18 @@
1
1
  # Authoring a Repository
2
2
 
3
- This is the guide to publishing recipes of your own: creating a repository, writing a recipe,
4
- editing one that is already published, cutting a release, and proposing a change to someone
5
- else's repository. [Repository file formats](repositories-file-formats.md) holds every schema
6
- these commands read and write; this page is about the workflow.
3
+ The guide to publishing recipes of your own: creating a repository, writing a recipe, declaring the variables it
4
+ needs, cutting a release, editing a published repository in place, and proposing a change to somebody else's.
5
+ [Repository file formats](repositories-file-formats.md) holds the schemas; this page is the workflow.
7
6
 
8
- ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init`,
9
- `sous repo release` and `sous repo submit` do not look for one. Run them from inside the
10
- repository itself.
7
+ ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init`, `sous repo
8
+ release` and `sous repo submit` do not look for one. Run them from inside the repository itself.
11
9
 
12
10
  ## Create a repository
13
11
 
14
12
  ```term
15
13
  $ sous repo init ./my-recipes --name my-recipes --namespace workflow
14
+ ▶ Creating a recipe repository:
15
+
16
16
  wrote sous.repo.yaml
17
17
  wrote sous.index.json
18
18
  wrote recipes/workflow/example/sous.recipe.yaml
@@ -22,52 +22,26 @@ $ sous repo init ./my-recipes --name my-recipes --namespace workflow
22
22
  wrote .gitignore
23
23
  ```
24
24
 
25
- | Flag | What it does |
26
- |------|--------------|
27
- | `--name <name>` | Short name for the repository. Defaults to the directory's own name |
28
- | `--namespace <name>` | The one namespace to declare. Defaults to the repository's name |
29
- | `--force` | Write the scaffold over a repository that already exists |
30
- | `--dry-run` | Print the files that would be written without writing them |
31
-
32
- Names are lowercase kebab-case, and a name given in any other case is lowercased for you, so a
33
- directory called `My-Recipes` yields `my-recipes`. `repo init` refuses to write over a directory
34
- that already holds a repo manifest unless you pass `--force`, and it reads every manifest back
35
- after writing it, so the scaffold it leaves behind is one that validates.
36
-
37
- ?> `repo init --force` means overwrite, not "answer yes". It is unrelated to the shared
38
- confirmation flag other commands spell `--yes`, `-y`, `--force` or `--trust`, and `repo init`
39
- does not accept those other spellings.
40
-
41
- ## The layout
42
-
43
- ```text
44
- my-recipes/
45
- sous.repo.yaml what this repository publishes
46
- sous.index.json the catalog, written by 'sous repo release'
47
- recipes/
48
- workflow/
49
- example/
50
- sous.recipe.yaml one recipe, with its own version
51
- skills/
52
- example-skill/
53
- SKILL.md
54
- .github/workflows/sous-release.yml
55
- .gitignore
56
- README.md
57
- ```
25
+ `--name` is the repository's short name (default: the directory's own name), `--namespace` the one namespace to
26
+ declare (default: the repository's name), `--force` overwrites a repository that already exists, and `--dry-run`
27
+ lists the files without writing them. The command asks nothing, and names are lowercase kebab-case, so `My-Recipes`
28
+ yields `my-recipes`. Every file is read back through the schemas sous uses on published repositories, so the
29
+ scaffold validates. The scaffold is files only: run `git init`, commit it, and add the remote you publish from
30
+ before releasing, since `sous repo release` reads and writes tags and fails in a directory git does not track.
58
31
 
59
- Nothing in that tree is fixed except the two manifest filenames and the index at the root.
60
- Recipe folders may live anywhere; what makes a folder a recipe is that `sous.repo.yaml` lists its
61
- path under `recipes`, and that the folder holds exactly one recipe manifest.
32
+ ?> `repo init --force` means overwrite, not "answer yes". It is unrelated to the confirmation flag other
33
+ commands spell `--yes`, `-y`, `--force` or `--trust`.
62
34
 
63
- Both hand-written manifests are YAML or JSON and never JavaScript. A repository's whole trust
64
- story rests on being readable without running any of its code, and a manifest that could execute
65
- would break that guarantee.
35
+ `sous.repo.yaml` says what the repository publishes, `sous.index.json` is the catalog that `sous repo release`
36
+ writes, and each recipe folder carries one `sous.recipe.yaml` beside the files it contributes. Nothing else is
37
+ fixed: recipe folders may live anywhere, and what makes a folder a recipe is that `sous.repo.yaml` lists its
38
+ path under `recipes`. Both hand-written manifests are YAML or JSON and never JavaScript, because a repository's
39
+ trust story rests on being readable without running any of its code.
66
40
 
67
41
  ## Write a recipe
68
42
 
69
- Copy the example folder, edit its manifest, and add the new path to the `recipes` list in
70
- `sous.repo.yaml`. A minimal recipe:
43
+ Copy the example folder, edit its manifest, and add its path to the `recipes` list in `sous.repo.yaml`, which is a
44
+ plain list of folder paths (`recipes:`, then `- recipes/workflow/task-files` under it):
71
45
 
72
46
  ```yaml
73
47
  formatVersion: 1
@@ -76,173 +50,125 @@ name: task-files
76
50
  version: 0.1.0
77
51
  description: >-
78
52
  Per-branch task files, with skills for starting and resuming work.
79
-
80
53
  contents:
81
54
  - kind: skills
82
55
  include:
83
56
  - skills/**/*.md
84
57
  ```
85
58
 
86
- `contents` groups are what a subscriber's project actually receives, one group per kind
87
- (`skills`, `memories`, `prompts` or `config`), each with `include` glob patterns relative to the
88
- recipe folder. Two more optional lists say what else the recipe needs: `depends` for build
89
- dependencies whose files stay out of a subscriber's output, and `subscribes` for co-subscriptions
90
- whose files go in. Full field tables are in
91
- [`sous.recipe.yaml`](repositories-file-formats.md#sousrecipeyaml-the-recipe-manifest).
59
+ `contents` groups are what a subscribing project receives, one group per kind, each with `include` glob patterns
60
+ relative to the recipe folder (and optional `exclude` patterns). The four kinds are `skills`, `memories`,
61
+ `prompts` and `config`; the kind decides where files land, which the subscriber maps with
62
+ [`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land). Field tables are in the
63
+ [recipe manifest](repositories-file-formats.md#sousrecipeyaml-the-recipe-manifest) reference, and recipe
64
+ metadata is the source of truth for the version: never edit `sous.index.json` by hand.
92
65
 
93
- Recipe metadata is the source of truth for the version. Never edit `sous.index.json` by hand;
94
- `sous repo release` regenerates it.
66
+ A recipe's files compile exactly the way a project's own `entryGlob` target does, so the
67
+ [`.tpl.` convention](configuration.md#templates-and-the-tpl-convention) applies unchanged inside a recipe. A
68
+ template may include a file from a recipe this one depends on through the reserved `~` sigil, resolved against
69
+ this recipe's own `depends` and `subscribes` at their pinned versions; see
70
+ [Including recipe files in your own templates](repositories.md#including-recipe-files-in-your-own-templates).
95
71
 
96
72
  ## Declare the variables a recipe needs
97
73
 
98
- A recipe that needs a value from the project asks for it through a **definition**: a published
99
- specification, never a value. Sous asks the question only when a subscribed recipe needs the
100
- variable and no valid answer is already in scope.
74
+ A recipe that needs a value from the project asks through a **definition**: a published specification, never a
75
+ value. Sous asks the question only when a subscribed recipe needs the variable and no valid answer is in scope.
101
76
 
102
77
  ```yaml
103
78
  variables:
104
- - name: taskFileRoot
79
+ - name: qaTaskRoot
105
80
  type: path
106
- prompt: Where should task files be stored?
81
+ prompt: Where should the review notes be stored?
107
82
  description: >-
108
- This recipe mandates the creation of task files that are stored locally
109
- and, in general, should not be committed. This setting dictates the path in
110
- which agents will store and search for your task files. The default value
111
- stores task files in the project's .sous directory, but you can specify any
112
- local path, either relative to the project root or absolute.
113
- example: ~/my-task-files
114
- default: .sous/tasks
83
+ This recipe writes one review note per branch, and this setting is the
84
+ directory those notes are read from and written to. The default keeps them
85
+ inside the project's .sous directory, which git already partly ignores.
86
+ Any local path works, relative to the project root or absolute.
87
+ example: ~/qa-notes
88
+ default: .sous/qa-notes
115
89
  required: true
116
90
  scope: shared
117
91
 
118
- - name: serviceToken
92
+ - name: qaServiceToken
119
93
  type: string
120
- env: SERVICE_TOKEN
121
- prompt: What is this project's service token?
94
+ env: QA_SERVICE_TOKEN
95
+ prompt: What is this project's review service token?
122
96
  description: >-
123
- This recipe authenticates every call it makes with a service token, which
124
- is issued per project and is not shared between them. Create one under
125
- Settings, then Tokens, and give it read access to the project you are
126
- configuring. There is no default; a token is always specific to you, and it
127
- is stored in the gitignored env file so it never reaches git.
128
- example: svc_0123456789abcdef0123
97
+ The review service is called once per run and authenticates with a token
98
+ issued per project. There is no default, because a token belongs to one
99
+ person on one machine; create one under Settings, then Tokens.
100
+ example: qa_0123456789abcdef0123
129
101
  secret: true
130
102
  scope: local
131
103
  validate:
132
104
  minLength: 20
133
105
  ```
134
106
 
135
- The rules worth knowing while you write one:
136
-
137
- - **`name` is camelCase**, and it is how templates refer to the variable.
138
- - **`description` and `example` are both required.** The one-line `prompt` is rarely enough on its
139
- own, and the person answering it cannot read your mind. The description is the paragraph shown
140
- above the question and by `sous vars show <name>`; the example is a realistic sample answer,
141
- shown with the question.
142
- - **A description explains, a prompt asks.** Write the description in full sentences, and cover
143
- three things: what the setting is for, what the default does, and what else is acceptable. Write
144
- the prompt as one plain question and nothing else. The pair above is the model:
145
- "This recipe mandates the creation of task files that are stored locally and, in general, should
146
- not be committed. This setting dictates the path in which agents will store and search for your
147
- task files. The default value stores task files in the project's .sous directory, but you can
148
- specify any local path, either relative to the project root or absolute." asked as
149
- "Where should task files be stored?". A description that only restates the prompt, or a prompt
150
- that tries to carry the explanation, both make the question harder to answer.
151
- - **An example is documentation, a default is a value.** Sous never stores an example and never
152
- offers it as the answer; it only ever shows it. Use `default` for a value a project should
153
- actually start with. The same text may appear in both when the sample answer really is the right
154
- starting value.
155
- - **`env` names the environment variable** an answer binds to. Omit it and `sous repo release`
156
- derives one from the name: `apiUrl` becomes `SOUS_VAR_API_URL`. Naming it explicitly is how a
157
- recipe reuses a value the environment already carries, such as `GITHUB_TOKEN`.
158
- - **`secret: true`** stores the answer in the gitignored `.sous/.env.local` and hides the value
159
- everywhere sous prints it.
160
- - **`scope`** picks the file the answer is written to: `shared` for the committed `.sous/.env`,
161
- `local` for the gitignored `.sous/.env.local`. A secret declared as `shared` is rejected,
162
- because that combination would commit the secret.
163
- - **`validate.pattern` runs under a time budget.** Sous runs a published pattern on a worker and
164
- stops waiting after a fixed budget, so a pattern that backtracks forever cannot hang the person
165
- answering; it fails validation instead, and the message names your pattern. Keep patterns simple.
166
- - **`x-intentional: true`** silences the release warning about claiming a well-known environment
167
- variable name. `PATH`, `HOME`, `USER`, `SHELL`, `GITHUB_TOKEN`, `GITLAB_TOKEN`, `NPM_TOKEN` and
168
- anything starting `AWS_` or `SOUS_` draw that warning; binding an existing token is legitimate,
169
- it just deserves saying out loud.
170
-
171
- Two definitions of the same name may share one environment variable, because that is exactly
172
- what the shared rung of the resolution ladder is for. Two definitions of **different** names
173
- claiming the same environment variable is an error. See
174
- [Recipe variables](repositories-variables.md) for how an answer is found at build time.
175
-
176
- !> Within a major version a schema may only LOOSEN. Tightening a constraint is a major bump, and
177
- an upgrade re-validates stored answers, re-prompting only where an old answer no longer fits.
178
-
179
- ## Edit a repository in place
180
-
181
- Edits happen in a real working copy, never in the machine-wide store. `sous repo link`, run
182
- inside a project, points that project's resolution of one repository at a checkout:
183
-
184
- ```bash
185
- sous repo link ~/Projects/my-recipes # link the checkout that is already there
186
- sous repo link my-recipes # clone it into .sous/repos/<owner>/<name>
187
- sous repo link my-recipes ~/Projects/my-recipes # link a checkout that already exists
188
- sous repo link my-recipes --global # one checkout shared by every project
189
- sous repo unlink my-recipes
190
- ```
191
-
192
- The command is written three ways.
193
-
194
- A **path on its own** links the checkout that is already at that path, where it is. Relative
195
- paths and `~` work, because that is what people type. The repository is added to the project
196
- first if it has not been added yet, which is the same trust ceremony `sous repo add` runs; its
197
- short name is the one the checkout's own repo manifest suggests, falling back to the directory's
198
- name. Nothing is cloned.
199
-
200
- A **repository on its own** is cloned for you, into `.sous/repos/<owner>/<name>` or into
201
- `$SOUS_HOME/repos/<owner>/<name>` with `--global`. A directory that is already a checkout of the
202
- same remote is reused rather than cloned again, so running the command twice is harmless; one
203
- holding a different remote is an error, because reading the wrong recipes silently would be
204
- worse than stopping.
107
+ - **`name` is camelCase**, and it is how templates refer to the variable. `type` is `string`, `number`,
108
+ `boolean`, `enum`, `path` or `url`. `required` defaults to true.
109
+ - **A description explains, a prompt asks.** Both `description` and `example` are required, and a manifest missing
110
+ either is refused. Write the description in full sentences: what the setting is for, what the default does, and
111
+ what else is acceptable; write the prompt as one plain question. It is also what `sous vars show <name>` prints.
112
+ - **An example is documentation, a default is a value.** Sous never stores an example and never offers it as
113
+ the answer; it only shows it beside the question. Use `default` for a value a project should actually start
114
+ with, and put the same text in both only when the sample answer is the right starting value.
115
+ - **`env` is the environment variable** an answer binds to. Omit it and the answer binds to the shared rung of
116
+ the resolution ladder, derived from the name: `apiUrl` becomes `SOUS_VAR_API_URL`. Name it explicitly to reuse
117
+ a value the environment already carries, such as `GITHUB_TOKEN`; `x-intentional: true` then silences the
118
+ release warning about claiming a well-known name (`PATH`, `HOME`, `USER`, `SHELL`, `GITHUB_TOKEN`,
119
+ `GITLAB_TOKEN`, `NPM_TOKEN`, or anything starting `AWS_` or `SOUS_`, except the `SOUS_VAR_` names sous
120
+ derives itself). Two definitions of the same name may share one variable; two of **different** names
121
+ claiming the same one is an error.
122
+ - **`secret: true`** hides the value everywhere sous prints it, and **`scope`** picks the file the answer is
123
+ written to: `shared` for the committed `.sous/.env`, `local` for the gitignored `.sous/.env.local`. A secret
124
+ is always stored locally, so a secret declared `shared` is rejected.
125
+ - **`validate`** carries `pattern`, `minLength`, `maxLength`, `min`, `max` and `enum`. A published `pattern` runs on
126
+ a worker under a time budget, so one that backtracks forever fails validation rather than hanging the person
127
+ answering, and the message names your pattern. Within a major version a schema may only LOOSEN: tightening a
128
+ constraint is a major bump, and an upgrade re-validates stored answers, re-prompting only where an old one no
129
+ longer fits.
130
+ - **Further reading:** [Recipe variables](repositories-variables.md) covers how an answer is found at build
131
+ time; [Variable definitions](repositories-file-formats.md#variable-definitions) is the full field table.
132
+
133
+ ## Declare dependencies and co-subscriptions
134
+
135
+ `depends` fetches and pins a recipe whose files stay OUT of a subscriber's output but are addressable from your
136
+ own templates. `subscribes` is a co-subscription: its questions run and its files DO enter the output, which is
137
+ how a curated bundle is built. Both hold plain strings naming their target by where it lives.
205
138
 
206
- A **repository followed by a path** links the checkout at that path to that repository, and
207
- clones nothing.
208
-
209
- In every form the path must hold a repo manifest at its root, and a path in both slots is an
210
- error: the first one already says which checkout to link.
211
-
212
- Linking also maintains the two ignore files that keep machine-local sous files out of your
213
- project's history: a `.sous/repos/.gitignore` holding a single `*`, and a delimited managed block
214
- inside `.sous/.gitignore`. Only the lines between the markers are ever rewritten, and both files
215
- are written only when their contents would change. `sous prune` and `sous clear` never reach
216
- into `.sous/repos/`.
217
-
218
- Because a link bypasses versions, the lockfile and freshness checks, and because those bypasses
219
- belong to one person's machine rather than to the team, both the link command and every
220
- subsequent build say so loudly:
221
-
222
- ```text
223
- The repository 'my-recipes' is now LINKED.
224
- Its recipes are read from the checkout above, so versions, the lockfile
225
- and freshness checks no longer apply to it. Builds say so every time.
139
+ ```yaml
140
+ depends:
141
+ - workflow/qa-helper # a sibling, released alongside me
142
+ - workflow/qa-helper@^1.1 # a sibling with a range; uncommon
143
+ - github://sous-io/sous-recipes/workflow/sat@^1.0 # another repository
144
+ - gitlab://gitlab.example.com/group/subgroup/proj/qa/lint # a self-hosted host, with subgroups
145
+ subscribes:
146
+ - quality/code-review # co-subscribed: its files land too
226
147
  ```
227
148
 
228
- `sous repo unlink` removes the map entry and nothing else. The checkout stays exactly where it
229
- is, and its path is printed so you can delete it yourself if you want to. Getting the scope wrong
230
- is the easy mistake here, so unlinking a name that is linked in the other scope tells you which
231
- scope holds it and which flag removes it.
149
+ A **bare ref** is a sibling in this repository; with no range it means "the version released alongside me", since
150
+ one run cuts both tags and records the exact version in the index. A locator URL names another repository,
151
+ and its last two segments are always the namespace and the recipe, which is the recipe's published **identity and
152
+ never a path on disk**: a recipe stored at `recipes/shared/sat/` and published as `workflow/sat` is written
153
+ `github://owner/repo/workflow/sat`. Everything before them is the repository, a dotted first segment naming the
154
+ host. The optional range after `@` follows npm's rules. `local://` locators and `repo:` short names are refused:
155
+ a local repository is a consumer's convenience, and a short name is one project's private label. See
156
+ [Dependencies named by location](repositories-file-formats.md#dependencies-named-by-location).
232
157
 
233
- ## Release
158
+ ## Cut a release
234
159
 
235
- `sous repo release` publishes new versions of this repository's recipes. One run does the whole
236
- job: it raises versions, regenerates `sous.index.json`, commits both, and cuts the tags that
237
- publish them. Run it from inside the repository, with your recipe changes already committed.
160
+ `sous repo release` publishes new versions: one run raises versions, regenerates `sous.index.json`, commits both,
161
+ and cuts the tags. Run it inside the repository, with your recipe changes committed.
238
162
 
239
163
  ```term
240
164
  $ sous repo release
241
- The release this would make:
242
- workflow/task-files : 1.1.0 becomes 1.1.1 (a patch step; the tag would be workflow/task-files@1.1.1)
165
+ ▶ The release this would make:
166
+
167
+ workflow/qa-helper: 0.1.0 becomes 0.1.1 (a patch step; the tag would be
168
+ workflow/qa-helper@0.1.1)
243
169
 
244
170
  Left alone:
245
- workflow/sat: its files have not changed since workflow/sat@1.4.0.
171
+ workflow/qa-variables: its files have not changed since workflow/qa-variables@0.1.0.
246
172
 
247
173
  This run would:
248
174
  Raise the versions listed above, in the manifests that declare them.
@@ -252,157 +178,140 @@ $ sous repo release
252
178
  Push nothing; pass '--push' to push what it makes.
253
179
 
254
180
  Publish these versions? yes
255
- workflow/task-files: 1.1.0 becomes 1.1.1.
256
- Wrote sous.index.json.
257
- Committed: Release workflow/task-files@1.1.1
258
- Created the tag workflow/task-files@1.1.1.
259
- ```
260
181
 
261
- The plan is always printed first, and a run asks once before it changes anything. `--yes` answers
262
- that question ahead of time, and `--dry-run` prints the plan and stops.
263
-
264
- ### What a run decides
265
-
266
- Three facts about each recipe decide everything, and nothing else does:
267
-
268
- 1. **Is it in scope?** Every recipe is, unless `--namespace` or `--recipe` narrows the run.
269
- 2. **Have its files changed since the tag that last published it?** A recipe nobody touched is
270
- not re-released; a published version that says the same thing as the one before it is noise.
271
- `--include-unchanged` releases everything in scope anyway.
272
- 3. **Has its version already been raised past that tag?** If so, the bump has been done and this
273
- run only publishes it. That is what a merge looks like to the continuous integration run.
274
-
275
- ### The flags
276
-
277
- | Invocation | What it does |
278
- |------------|--------------|
279
- | `sous repo release` | Plan, ask once, then bump, regenerate, commit and tag |
280
- | `sous repo release --dry-run` | Print the plan and stop |
281
- | `sous repo release --yes` | Skip the question; everything else is the same |
282
- | `sous repo release --namespace <ns>` | Release only that namespace. Repeatable |
283
- | `sous repo release --recipe <ns/name>` | Release only that recipe. Repeatable |
284
- | `sous repo release --bump <level>` | `patch` (the default), `minor`, `major` or `prerelease` |
285
- | `sous repo release --no-bump` | Raise nothing; a changed recipe nobody raised is an error |
286
- | `sous repo release --include-unchanged` | Release everything in scope, changed or not |
287
- | `sous repo release --tag` | Cut the tags even on a branch other than the default one |
288
- | `sous repo release --push` | Push the commit, and the tags this run created, to `origin` |
289
- | `sous repo release --check` | Read only: validate, and fail when the committed index is out of date |
290
- | `sous repo release --ci` | The merge preset: never bump, never ask, fail on anything unbumped |
182
+ ▶ Publishing:
291
183
 
292
- ### The branch rule
184
+ workflow/qa-helper: 0.1.0 becomes 0.1.1.
185
+ Wrote sous.index.json.
186
+ Committed: Release workflow/qa-helper@0.1.1
187
+ Created the tag workflow/qa-helper@0.1.1.
188
+ ```
293
189
 
294
- On a branch other than the default one, a release bumps and commits but cuts no tags, and says
295
- why: tags are cut on the default branch, by continuous integration after the merge. Pass `--tag`
296
- to cut them anyway, which is what a repository with no automation wants.
190
+ That run leaves out the banner and the checking step, and prints how to push once it finishes. The plan comes first
191
+ and a run asks once before changing anything; `--yes` answers ahead of time and `--dry-run` prints the plan and
192
+ stops. Three facts about each recipe decide that plan and nothing else does: whether it is in scope, whether its
193
+ files changed since the tag that last published it, and whether its version was already raised past that tag. The
194
+ third case is what a merge looks like to continuous integration: the bump is done, so the run only publishes it.
297
195
 
298
- ### Dependencies and the sibling rule
196
+ `--namespace <ns>` and `--recipe <ns/name>` narrow the run and both repeat; `--bump <level>` is `patch` (the
197
+ default), `minor`, `major` or `prerelease`; `--no-bump` raises nothing; `--include-unchanged` releases every
198
+ recipe in scope, changed or not; `--check` only reads, validating and failing when the committed index is stale;
199
+ and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
200
+ this command takes, `--tag`, `--push` and `--non-interactive` among them, is in the
201
+ [command reference](commands.md#sous-repo-release).
299
202
 
300
- Tags are cut **dependency-first**, so a recipe is never published before something it depends on.
301
- Everything a released recipe depends on inside this repository has to be a version that exists
302
- once the run's own tags are counted, and there are exactly two ways that fails:
203
+ `--bump` and `--no-bump` contradict each other, and so do `--check` and `--ci`; sous refuses the combination and
204
+ says which one to keep. `--ci` implies `--no-bump` and `--yes`, accepting the plan it prints rather than asking,
205
+ but it deliberately does not imply `--push`, so what a workflow pushes stays visible in the workflow file. Under
206
+ either, a recipe whose files changed without a bump is an error naming its manifest and the colliding tag.
303
207
 
304
- - The sibling has **never been published**. Nothing can depend on it, so the run stops and names
305
- the tag that has to be cut.
306
- - The sibling has been published, has changed since, and sits **outside this release's scope**.
307
- That is fine: the release goes ahead depending on the last published version, and warns with
308
- facts you can check.
208
+ ### The branch rule
309
209
 
310
- ```term
311
- recipes/workflow/task-files/sous.recipe.yaml:
312
- 'workflow/sat' has changes since 'workflow/sat@1.4.0' that are outside this release's scope;
313
- 'workflow/task-files@1.1.1' will depend on 'workflow/sat@1.4.0'.
314
- ```
210
+ The default branch is whatever `origin/HEAD` points at. On any other branch a release bumps and commits but cuts
211
+ no tags, and says why: tags are cut on the default branch, by continuous integration after the merge. Pass
212
+ `--tag` to cut them anyway, which is what a repository with no automation wants; a clone that was never told its
213
+ default branch (one with no remote) treats every branch as the default.
315
214
 
316
- Each version's resolved dependencies are written into the index, so a consumer installing that
317
- version installs what it was published with rather than re-resolving its ranges months later.
215
+ ### The sibling rule
318
216
 
319
- ### Three rules that keep metadata, tags and the index in step
217
+ Tags are cut **dependency-first**, so a recipe is never published before something it depends on, and every
218
+ sibling it depends on must be a version that exists once the run's own tags are counted. A sibling **never
219
+ published** stops the run, naming the tag to cut. One that was published, has changed since, and sits **outside
220
+ this release's scope** is fine: the release depends on the last published version, and warns.
320
221
 
321
- - **A published version never changes.** Its content hash is carried forward exactly as
322
- published, and a disagreement is an error telling you to bump the version rather than
323
- republish it.
324
- - **A version is published when its tag exists.** The one moment an index records a version
325
- without a tag is the release commit itself: the index is committed and the tag is cut on that
326
- commit. Any older version missing its tag is an error.
327
- - **The tags are the backstop.** A tagged version missing from the index is rebuilt from its tag,
328
- so deleting `sous.index.json` and regenerating it restores the same catalog.
222
+ ```text
223
+ WARNING:
224
+ recipes/workflow/qa-variables/sous.recipe.yaml:
225
+ 'workflow/qa-helper' has changes since 'workflow/qa-helper@0.1.0' that are outside this
226
+ release's scope; 'workflow/qa-variables@0.1.1' will depend on 'workflow/qa-helper@0.1.0'.
227
+ ```
329
228
 
330
- !> A release commits the version bumps and the index, and nothing else. It refuses to run while
331
- anything else is uncommitted, because a tag names one commit and the index it writes records
332
- what each recipe folder holds right now. It also refuses, before writing anything, when git has
333
- no author identity to commit under; set `user.name` and `user.email` in the repository, which the
334
- scaffolded workflow does for you.
229
+ Each version's resolved dependencies are written into the index, so a consumer installing that version gets what
230
+ it was published with rather than ranges re-resolved months later. A published version never changes: its content
231
+ hash is carried forward exactly as published, and a disagreement is an error telling you to bump rather than
232
+ republish. A version counts as published once its tag exists, and the tags are the backstop, so a tagged version
233
+ missing from the index is rebuilt from it whenever the index is regenerated, which only happens on a run that
234
+ publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
235
+ block of YAML prose may be re-wrapped and the space before a trailing comment collapsed to one.
335
236
 
336
- A bump edits the manifest in place, so its comments, its field order and its layout all survive.
337
- Two small normalizations happen in a YAML manifest: a folded block of prose may be re-wrapped,
338
- and the spacing before a trailing comment is collapsed to one space.
237
+ !> A release commits the version bumps and the index, and nothing else. It refuses to run while anything else is
238
+ uncommitted, because a tag names one commit and the index records what each recipe folder holds right now. It also
239
+ refuses when git does not know who is committing: set `git config user.name` and `git config user.email` first, or
240
+ give that identity to the account a continuous integration job runs as (the scaffolded workflow already does).
339
241
 
340
242
  ### The scaffolded workflow
341
243
 
342
- `sous repo init` writes `.github/workflows/sous-release.yml`, which runs the same command in its
343
- two presets. It calls the sous CLI straight from npm, so nothing has to be installed into the
344
- repository:
345
-
346
- - On a **pull request**, `sous repo release --check`. It only reads, so it is safe on an
347
- untrusted branch, and it fails the pull request when a manifest is wrong or the committed index
348
- (including the dependencies it records) is stale.
349
- - On a **push to the default branch**, `sous repo release --ci --push`. `--ci` raises no versions
350
- and asks no questions: the version bump belongs in the change being merged, so a recipe that
351
- changed without one fails here rather than being given a version nobody reviewed. Both
352
- checkouts use `fetch-depth: 0`, so existing tags are visible and a published version is never
353
- cut a second time.
244
+ `sous repo init` writes `.github/workflows/sous-release.yml`, which runs the same command in its two presets,
245
+ calling the CLI from npm so nothing is installed into the repository. On a **pull request** it runs
246
+ `npx --yes @sous-io/sous repo release --check`, which only reads, so it is safe on an untrusted branch and
247
+ fails the pull request when a manifest is wrong or the committed index is stale. On a **push to `main`** it
248
+ runs `npx --yes @sous-io/sous repo release --ci --push --yes`; edit the workflow's `branches` list if your
249
+ default branch has another name. `--ci` accepts the plan on its own, the redundant `--yes` keeping the file
250
+ working with an older published sous. Both jobs check out with `fetch-depth: 0`, so existing tags are visible
251
+ and no version is cut twice; the release job also takes `contents: write` and sets a git identity for the commit.
354
252
 
355
- ## Contribute to someone else's repository
253
+ ## Edit a repository in place
356
254
 
357
- `sous repo submit` proposes your committed changes to a repository's maintainers. It never
358
- publishes and never writes to a repository directly.
255
+ This is also how a recipe you have not released yet is run: link the checkout into a project and build. Edits
256
+ happen in a real working copy, never in the machine-wide store. `sous repo link`, run inside a **project**,
257
+ points that project's resolution of one repository at a checkout:
359
258
 
360
259
  ```bash
361
- sous repo submit
362
- sous repo submit --title "Add a linting recipe"
363
- sous repo submit --draft
364
- sous repo submit --dry-run
260
+ sous repo link ~/Projects/my-recipes # link the checkout that is already there
261
+ sous repo link my-recipes # clone it into .sous/repos/<owner>/<name>
262
+ sous repo link my-recipes ~/Projects/my-recipes # link a checkout that already exists
263
+ sous repo link my-recipes --global # one checkout shared by every project
365
264
  ```
366
265
 
367
- It runs in three stages, printing each step before it runs:
368
-
369
- 1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's
370
- command line tool (`gh` or `glab`) is installed and signed in, and everything is committed.
371
- 2. **Validation.** The repository validates and the committed index is current, so a proposal
372
- never fails the maintainer's own checks and wastes their review.
373
- 3. **Delegation.** Sous asks the provider whether you can push to the repository itself, forks
374
- it onto your account when you cannot, pushes the branch, and asks the provider to open the
375
- proposal. Every one of those is the provider's own business; sous only sequences them and
376
- reports what came back. A change sitting on the default branch is moved to a branch named
377
- `sous/submit-<date>-<time>` first.
266
+ A **path on its own** links the checkout already at that path; relative paths and `~` work, nothing is cloned, and
267
+ the repository is added to the project first if it has not been added yet, under the short name its own manifest
268
+ suggests. A **repository on its own** is cloned for you, into `.sous/repos/<owner>/<name>` or
269
+ `$SOUS_HOME/repos/<owner>/<name>` with `--global`; a directory already holding a checkout of the same remote is
270
+ reused, so running it twice is harmless, and one holding a different remote is an error. A **repository followed by
271
+ a path** links the checkout there and clones nothing; in every form the path must hold a repo manifest at its root,
272
+ and a path in both slots is an error.
378
273
 
379
- `--title` defaults to your last commit's subject and `--body` to a summary sous writes. A failure
380
- partway through says exactly which steps completed: a pushed branch with no proposal behind it is
381
- a normal outcome of a network failure, and you are told about it rather than left guessing.
274
+ Linking also maintains the ignore files that keep machine-local sous files out of your project's history: a
275
+ `.sous/repos/.gitignore` holding a single `*`, and a managed block inside `.sous/.gitignore` whose marked lines
276
+ are the only ones ever rewritten. A link bypasses versions, the lockfile and freshness checks, and those bypasses
277
+ belong to one machine rather than the team, so link and every later build say so loudly:
382
278
 
383
- ### What each provider supports
279
+ ```text
280
+ The repository 'my-recipes' is now LINKED.
281
+ Its recipes are read from the checkout above, so versions, the lockfile
282
+ and freshness checks no longer apply to it. Builds say so every time.
384
283
 
385
- Providers differ, and sous says so rather than pretending otherwise:
284
+ Run 'sous repo unlink my-recipes' to go back to published versions.
285
+ ```
386
286
 
387
- | Provider | Command line tool | Push permission | Forking | Proposal |
388
- |---|---|---|---|---|
389
- | GitHub | `gh` | Read from GitHub, so a contributor without it is forked automatically | `gh repo fork`, with a `fork` remote added for you | Pull request |
390
- | GitLab | `glab` | Sous cannot tell, so it pushes to `origin` and says so | Not done for you; fork the project yourself and push your branch there | Merge request |
391
- | Local | none | Not applicable | Not applicable | Not applicable; a repository on your own disk is edited directly |
287
+ `sous repo unlink` removes the map entry and nothing else: the checkout stays where it is, and its path is
288
+ printed so you can delete it. Unlinking a name linked in the other scope says which scope holds it.
392
289
 
393
- When a provider cannot carry out a step, it says what to do by hand instead of stopping halfway
394
- through. A local repository never submits at all: it declares no submit support, so sous points
395
- you at the repository's `contribute` field instead.
290
+ ## Contribute to someone else's repository
396
291
 
397
- ?> When a repository's provider cannot open a proposal for you, sous prints the `contribute`
398
- pointer from its `sous.repo.yaml` instead, so you are never left without a route. Set that field
399
- in your own repository for the same reason.
292
+ `sous repo submit` proposes your committed changes to a repository's maintainers; it never publishes and never
293
+ writes to a repository directly. It takes `--title`, `--body`, `--draft` and `--dry-run`, and runs three stages,
294
+ printing each step:
295
+
296
+ 1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's command line tool
297
+ (`gh` or `glab`) is installed and signed in, and everything is committed.
298
+ 2. **Validation.** The repository validates and the committed index is current, so a proposal never fails the
299
+ maintainer's own checks and wastes their review.
300
+ 3. **Delegation.** Sous asks the provider whether you can push to the repository itself, forks it onto your
301
+ account when you cannot, pushes the branch, and asks the provider to open the proposal. A change sitting on
302
+ the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`.
303
+
304
+ Each step is the provider's own business, and what each one can do depends on the host; see
305
+ [Providers](repositories-providers.md#proposing-a-change). Sous sequences the steps and reports what came back. A
306
+ failure partway through says which steps completed, so a pushed branch with no proposal behind it is reported
307
+ rather than left for you to guess at, and a provider that cannot carry out a step says what to do by hand. When it
308
+ cannot open a proposal at all, sous prints the `contribute` pointer from the repository's `sous.repo.yaml`; set
309
+ that field in your own repository so a contributor is never left without a route.
400
310
 
401
311
  ## Where to go next
402
312
 
403
313
  - [Repository file formats](repositories-file-formats.md): every manifest and index schema
404
- - [Recipe variables](repositories-variables.md): what a definition turns into on a subscriber's
405
- machine
406
- - [Skill categories](skill-categories.md): the canonical categories, and how the official
407
- repository uses them as namespaces
314
+ - [Consuming recipes](repositories-consuming.md): the other side, from `repo add` to `build`
408
315
  - [Command reference](commands.md): every command and flag
316
+ - [Providers](repositories-providers.md): what `gh` and `glab` can each do for a release and a submission
317
+ - [Recipe variables](repositories-variables.md): how the definitions declared here are answered at build time