@sous-io/sous 0.2.16 → 0.2.17

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.
@@ -17,7 +17,8 @@ Every command that works on a project takes these four. `SOUS_CONFIG`, `SOUS_DIR
17
17
  a flag beats its variable, and both beat walk-up discovery. See [Discovery and overrides](config-discovery.md).
18
18
 
19
19
  ?> `repo init`, `repo release` and `repo submit` take none of these. They run inside a recipe repository, which
20
- has no `.sous/` directory to discover. All three still take `--non-interactive`.
20
+ has no `.sous/` directory to discover. All three still take `--non-interactive`. `repo submit` may also be run
21
+ from a project, which it finds by walking up from the working directory.
21
22
 
22
23
  ## Flags that answer questions
23
24
 
@@ -254,17 +255,32 @@ and `git config user.email`), because it commits and cuts annotated tags. Takes
254
255
  - `--no-bump`: raise nothing; a changed recipe that was never raised is then an error.
255
256
  - `--include-unchanged`: release every recipe in scope, changed or not.
256
257
  - `--tag`, `--push`: tag even on a non-default branch, and push the commit and this run's tags.
257
- - `--check`: only validate. It fails on a problem the release would refuse, and reports, without failing, how
258
- merging would rewrite the committed index.
258
+ - `--check`: only validate. It fails on a problem the release would refuse, and on a change to a recipe that
259
+ takes no proposals (see [`submissions`](repositories-file-formats.md#the-submissions-block)), and reports,
260
+ without failing, how merging would rewrite the committed index.
259
261
  - `--ci`: the merge preset. Never bump, never ask, and fail on anything unbumped. It still needs `--yes` to
260
262
  accept the plan it prints, so a merge job runs `sous repo release --ci --yes --push`.
261
263
 
262
264
  Example: `sous repo release --recipe workflow/task-files --bump minor --push`
263
265
 
264
- ### `sous repo submit`
265
- Proposes this repository's committed changes to its maintainers. `--title <text>` defaults to the last commit's
266
- subject and `--body <text>` to a summary sous writes; `--draft` opens the proposal as a draft, and `--dry-run`
267
- prints the plan without sending anything. Example: `sous repo submit --title "Add a linting recipe" --draft`
266
+ ### `sous repo submit [REPO]`
267
+ Proposes a recipe repository's changes to its maintainers, and follows the proposal through: it opens one,
268
+ updates it when there is more to send, reports where it stands, and starts the next one once it was merged. Run
269
+ inside a recipe repository it works there; run inside a project, `REPO` names a linked repository and the
270
+ submission runs in its checkout (with no `REPO`, the only linked repository is used, and several are a
271
+ question). Takes `-y, --yes` and `--dry-run`.
272
+
273
+ - `--title <text>`, `--body <text>`: the proposal's title and description. Both are required for a new
274
+ proposal, and asked for at a terminal when missing; on an open proposal they are optional and replace its own.
275
+ The body is followed by a changelog sous generates.
276
+ - `--branch <name>`: work with this branch instead of the one checked out. A branch that does not exist is
277
+ created from the current commit.
278
+ - `--status`: only report where the branch's proposal stands; nothing is checked, written or sent.
279
+ - `--commit`: commit uncommitted changes for you, after listing them and asking once, with the title, the
280
+ description and the changelog as the message.
281
+ - `--draft`: open a new proposal as a draft.
282
+
283
+ Example: `sous repo submit --title "Add a linting recipe" --body "Adds lint rules for shell scripts." --draft`
268
284
 
269
285
  ## subscription
270
286
 
@@ -4,8 +4,9 @@ The guide to publishing recipes of your own: creating a repository, writing a re
4
4
  needs, cutting a release, editing a published repository in place, and proposing a change to somebody else's.
5
5
  [Repository file formats](repositories-file-formats.md) holds the schemas; this page is the workflow.
6
6
 
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.
7
+ ?> A recipe repository is not a sous project. It has no `.sous/` directory, and `sous repo init` and `sous repo
8
+ release` do not look for one. Run them from inside the repository itself. `sous repo submit` runs there too, and
9
+ may also be run from a project that links the repository.
9
10
 
10
11
  ## Create a repository
11
12
 
@@ -196,8 +197,9 @@ third case is what a merge looks like to continuous integration: the bump is don
196
197
  `--namespace <ns>` and `--recipe <ns/name>` narrow the run and both repeat; `--bump <level>` is `patch` (the
197
198
  default), `minor`, `major` or `prerelease`; `--no-bump` raises nothing; `--include-unchanged` releases every
198
199
  recipe in scope, changed or not; `--check` only reads, validating, failing on any problem the release would
199
- refuse, and reporting how merging would rewrite the committed index without failing on it, since that index is
200
- the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
200
+ refuse and on a change to a recipe whose [`submissions`](repositories-file-formats.md#the-submissions-block)
201
+ block says it takes no proposals, and reporting how merging would rewrite the committed index without failing
202
+ on it, since that index is the release's output; and `--ci` is the merge preset: never bump, accept the plan, never ask, fail on anything unbumped. Every flag
201
203
  this command takes, `--tag`, `--push` and `--non-interactive` among them, is in the
202
204
  [command reference](commands.md#sous-repo-release).
203
205
 
@@ -235,7 +237,8 @@ missing from the index is rebuilt from it whenever the index is regenerated, whi
235
237
  publishes something. A bump edits the manifest in place, so comments, field order and layout survive; a folded
236
238
  block of YAML prose may be re-wrapped and the space before a trailing comment collapsed to one.
237
239
 
238
- !> A release commits the version bumps and the index, and nothing else. It refuses to run while anything else is
240
+ !> A release commits the version bumps and the index, and nothing else; the only other commit sous ever makes
241
+ for you is `sous repo submit --commit`. It refuses to run while anything else is
239
242
  uncommitted, because a tag names one commit and the index records what each recipe folder holds right now. It also
240
243
  refuses when git does not know who is committing: set `git config user.name` and `git config user.email` first, or
241
244
  give that identity to the account a continuous integration job runs as (the scaffolded workflow already does).
@@ -344,22 +347,60 @@ changing its branch says it affects all of them.
344
347
 
345
348
  ## Contribute to someone else's repository
346
349
 
347
- `sous repo submit` proposes your committed changes to a repository's maintainers; it never publishes and never
348
- writes to a repository directly. It takes `--title`, `--body`, `--draft` and `--dry-run`, and runs three stages,
349
- printing each step:
350
+ `sous repo submit` proposes a change to a repository's maintainers and follows it through; it never publishes
351
+ and never writes to a repository directly. Run it inside the recipe repository you changed, or from a project
352
+ that links it: `sous repo submit sous-recipes` runs in the linked checkout, and with no argument the project's
353
+ only linked repository is used (several are a question). A repository you have since unlinked is still
354
+ submitted from the checkout sous cloned for it, with a note saying so; with no checkout at all there is no
355
+ working copy to propose from, and `submit` says so.
356
+
357
+ It prints each step as it runs them:
350
358
 
351
359
  1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's command line tool
352
- (`gh` or `glab`) is installed and signed in, and everything is committed.
360
+ (`gh` or `glab`) is installed and signed in, and everything is committed. `--commit` lifts that last rule:
361
+ sous lists what is uncommitted, asks once (`--yes` answers), checks git knows who is committing before
362
+ writing anything, and commits it all with the proposal's title, description and changelog as the message.
353
363
  2. **Validation.** The repository validates, and your change leaves `sous.index.json` as it found it, so a
354
364
  proposal never fails the maintainer's own checks and wastes their review. The index is written by the
355
365
  repository's own release after a merge; whether it agrees with the release tags is checked there, by
356
366
  `sous repo release --check` on a full clone, not by `submit`. That is what lets `submit` run from the shallow
357
367
  checkout `sous repo link` makes, which holds almost none of the tags. The comparison is made against the
358
368
  copy of the default branch your checkout holds (`origin/main`, for example); when it holds none, `submit`
359
- says the check was skipped.
369
+ says the check was skipped. A change that touches a recipe whose
370
+ [`submissions`](repositories-file-formats.md#the-submissions-block) block says it takes no proposals is
371
+ warned about, with where to send it instead, and proposed only if you carry on.
360
372
  3. **Delegation.** Sous asks the provider whether you can push to the repository itself, forks it onto your
361
373
  account when you cannot, pushes the branch, and asks the provider to open the proposal. A change sitting on
362
- the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`.
374
+ the default branch is moved to `sous/submit-<YYYYMMDD>-<HHMM>`, or to the branch `--branch` names.
375
+
376
+ **A title and a description are yours to write.** A new proposal needs both: pass `--title` and `--body`, or
377
+ answer the two questions at a terminal (Tab opens your editor for a longer description). Sous never borrows a
378
+ commit message. The body is your description followed by a changelog sous generates by comparing the manifests
379
+ your change carries with the default branch:
380
+
381
+ - recipes added, and recipes retired (a renamed recipe shows as one of each);
382
+ - version changes;
383
+ - recipes whose files changed without a version raise, which merging will release as the next patch;
384
+ - namespaces added or removed;
385
+ - variables added, removed or changed, with a warning that removing a variable or tightening its validation is
386
+ usually a major change.
387
+
388
+ The changelog explains; it never refuses. Only what the repository's own checks would reject stops a submission.
389
+
390
+ **One command for the proposal's whole life.** Every run looks up the proposal for the current branch (or the
391
+ one `--branch` names), by the fork's owner as well as the branch when you work through a fork, and then:
392
+
393
+ | The branch's proposal | What `submit` does |
394
+ |-----------------------|--------------------|
395
+ | none | Opens one |
396
+ | open, with new commits | Pushes them, which updates it; a given `--title` or `--body` replaces its own |
397
+ | open, with nothing new | Reports where it stands: review, checks, whether it can merge |
398
+ | merged | Says so, then continues on a new branch: you name one, sous generates one, or you cancel. `--yes` generates one |
399
+ | closed without merging | Says so, and opens a fresh one for the branch |
400
+
401
+ When the branch on the remote holds commits yours lacks (a maintainer pushed to it), git refuses the push and
402
+ `submit` passes git's own explanation through and stops. It never forces a push. `--status` only reports, and
403
+ `--dry-run` works everything out and writes and sends nothing.
363
404
 
364
405
  Each step is the provider's own business, and what each one can do depends on the host; see
365
406
  [Providers](repositories-providers.md#proposing-a-change). Sous sequences the steps and reports what came back. A
@@ -54,6 +54,7 @@ recipes: # every recipe folder, relative to the repos
54
54
  | `description`, `contribute` | no | A summary for anyone reading the repository, and where to send a contribution when a provider cannot support `sous repo submit` |
55
55
  | `namespaces` | yes | Keyed by namespace name; each entry takes an optional `description` |
56
56
  | `recipes` | yes | Relative paths, each holding a recipe manifest; a path listed twice is an error |
57
+ | `submissions` | no | Whether the repository's recipes take proposed changes; see [The submissions block](#the-submissions-block) |
57
58
 
58
59
  ## `sous.recipe.yaml`: the recipe manifest
59
60
 
@@ -77,11 +78,30 @@ contents: # what a subscriber actually receives
77
78
  | `description` | no | Copied into the index at release time; shown by `sous recipe list` and `sous repo search` |
78
79
  | `depends`, `subscribes` | no | Build dependencies and co-subscriptions; within either list, no entry may repeat |
79
80
  | `contents`, `variables` | no | Contents default to an empty list, which is what a curated bundle publishes; a duplicated variable name, or two definitions claiming one `env`, is an error |
81
+ | `submissions` | no | Whether this recipe takes proposed changes, winning over the repository's block; see [The submissions block](#the-submissions-block) |
80
82
 
81
83
  `contents[].kind` is `skills`, `memories`, `prompts` or `config`. The first three are written to the
82
84
  destinations named by [`recipeOutputs`](#recipeoutputs-where-the-files-land); `config` entries become config
83
85
  layers instead. `include` needs at least one glob, and both lists are recipe-relative.
84
86
 
87
+ ### The submissions block
88
+
89
+ Both manifests accept a `submissions` block. On `sous.repo.yaml` it covers every recipe in the repository; on
90
+ `sous.recipe.yaml` it covers that recipe, and wins over the repository's.
91
+
92
+ ```yaml
93
+ submissions:
94
+ allowed: false # defaults to true
95
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
96
+ ```
97
+
98
+ It exists for a recipe whose files are copied in from somewhere else: an edit merged into the copy is
99
+ overwritten by the next copy, and a version it tagged can collide with the one the real source publishes.
100
+ `sous repo submit` warns before proposing a change that touches such a recipe, prints the `instead` text, and
101
+ proposes it if the contributor carries on. `sous repo release --check` fails a pull request that changes one,
102
+ because a pull request can be opened without `submit`, and the check is the one gate every change passes. A
103
+ release itself is never restricted, so whatever publishes the recipe still releases it.
104
+
85
105
  ### Dependencies named by location
86
106
 
87
107
  `depends` and `subscribes` share one grammar in two spellings: `workflow/qa-helper` is a sibling in this
@@ -21,8 +21,12 @@ Nothing there clones a whole repository. A provider that can also carry a contri
21
21
  the **write path**: report whether its command line tool is installed and signed in, say whether
22
22
  you may push to the repository itself, fork it onto your account, and open the proposal.
23
23
 
24
- Each provider declares which of the two features, `fetch` and `submit`, it genuinely answers, and
25
- sous consults that declaration rather than a provider's name. Asking for something outside a
24
+ A provider may also answer the **proposals** path, which is what lets a submission follow a proposal
25
+ after it is opened: find the proposal a branch was pushed for, report where it stands, and replace its
26
+ title or body.
27
+
28
+ Each provider declares which of the three features, `fetch`, `submit` and `proposals`, it genuinely
29
+ answers, and sous consults that declaration rather than a provider's name. Asking for something outside a
26
30
  provider's features is refused with a sentence naming it and what it cannot do, never a crash.
27
31
 
28
32
  ## How a URL is matched
@@ -184,11 +188,15 @@ finds the index but cannot clone has only the second half missing.
184
188
  your repository, then hands the host-specific mechanics to the provider that owns its `origin`
185
189
  remote. Providers differ, and sous says so rather than pretending otherwise:
186
190
 
187
- | Provider | Tool | Push permission | Forking | Proposal |
188
- |---|---|---|---|---|
189
- | `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 |
190
- | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request |
191
- | `local` | none | not applicable | not applicable | not applicable |
191
+ | Provider | Tool | Push permission | Forking | Proposal | Following it up |
192
+ |---|---|---|---|---|---|
193
+ | `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 | `gh pr list`, `gh pr view` and `gh pr edit`, matching a fork's pull request by its owner |
194
+ | `gitlab` | `glab` | sous cannot tell, so it pushes to `origin` and says so | not done for you | merge request | not done: every run opens a merge request, and `--status` is refused |
195
+ | `local` | none | not applicable | not applicable | not applicable | not applicable |
196
+
197
+ **GitLab cannot follow a proposal up yet.** It does not declare `proposals`, so a submission cannot
198
+ look for a merge request that is already open; it says so, pushes, and opens one. When the branch
199
+ already has one, the push updates it and GitLab may refuse the second.
192
200
 
193
201
  **GitLab reports "cannot tell" rather than guessing.** Sous has no cheap, reliable way to ask
194
202
  whether you may push, and a wrong guess would send you down a fork path this provider cannot
@@ -297,14 +305,16 @@ subclass overrides it. Each member is documented where it lives, in
297
305
  The subclass supplies the rest:
298
306
 
299
307
  - `id`, the identifier a repository entry and a locator scheme use;
300
- - `features`, the ones it genuinely answers (`fetch`, and `submit` only if all four write calls
301
- are real);
308
+ - `features`, the ones it genuinely answers (`fetch`; `submit` only if all four write calls are
309
+ real; `proposals` only if `findProposal`, `proposalStatus` and `updateProposal` are);
302
310
  - `matches(url)` and `canonicalize(url)`, the URL half;
303
311
  - `fetchIndex(repo, options)` and `fetchRecipeTree(repo, recipePath, tag, destDir, options)`, the
304
312
  read half;
305
313
  - `cli` and `proposalNoun` when it submits, so messages can name the tool and call a proposal
306
314
  what the host calls it;
307
- - overrides of the four write-path calls when it submits.
315
+ - overrides of the four write-path calls when it submits, and of the three proposals calls when
316
+ it can follow a proposal up. Each answers with plain data (a proposal's id, address, state,
317
+ title, review and check counts), never with a host's own vocabulary.
308
318
 
309
319
  Every call takes an options object carrying the testing seams (`cwd`, `env`, `fetchImpl`, `run`),
310
320
  which is why no provider reaches for `spawn` or the global `fetch` directly and no test in this
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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",
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.16
14
+ version: 0.2.17
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -19,6 +19,13 @@ description: >-
19
19
  debugged, the .tpl. template convention and LiquidJS syntax, what an agent
20
20
  skill is, and how to create one.
21
21
 
22
+ # The copy in sous-io/sous-recipes is overwritten by every sous release, so a
23
+ # change proposed there would be lost, and a version it tagged would collide
24
+ # with the one the release publishes.
25
+ submissions:
26
+ allowed: false
27
+ instead: Propose changes in sous-io/sous, under recipes/core/sous-skills/.
28
+
22
29
  contents:
23
30
  - kind: skills
24
31
  include:
@@ -11,6 +11,7 @@ import {
11
11
  anythingToCommit,
12
12
  buildIndex,
13
13
  buildReleasePlan,
14
+ checkSubmissions,
14
15
  bumpRecipeVersion,
15
16
  commitPaths,
16
17
  createAnnotatedTag,
@@ -391,6 +392,46 @@ export default class RepoRelease extends Command {
391
392
  log(" records for every version it publishes.");
392
393
  }
393
394
  reportPending(result, "These versions have no tag yet; they publish when this merges:");
395
+
396
+ // A pull request can be opened without `sous repo submit`, so this check is
397
+ // the one gate every change passes. The release itself is not restricted:
398
+ // whatever publishes such a recipe still releases it.
399
+ section("Checking the recipes that take no proposals");
400
+ const submissions = await checkSubmissions(validation);
401
+ if (submissions.refusing.length > 0) {
402
+ const problems: ValidationProblem[] = submissions.refusing.map((recipe) => ({
403
+ level: "error",
404
+ where: recipe.path,
405
+ message:
406
+ `this change touches '${recipe.key}', which does not take proposed changes ` +
407
+ `(${recipe.declaredBy === "recipe" ? "its own manifest" : "the repository manifest"} ` +
408
+ `says so). Merging it would break whatever publishes the recipe.` +
409
+ (recipe.instead === undefined ? "" : ` Instead: ${recipe.instead}`),
410
+ }));
411
+ reportProblems(problems);
412
+ displayErrorBlock(
413
+ `This change cannot be merged: it touches ` +
414
+ `${describeCount(problems.length, "recipe")} that ${problems.length === 1 ? "does" : "do"} ` +
415
+ `not take proposed changes, listed above.\n` +
416
+ ` Take those edits out of the change, and send them where each recipe asks.`
417
+ );
418
+ return this.exit(1);
419
+ }
420
+ if (submissions.comparedWith.kind === "branch") {
421
+ log(
422
+ ` Compared with the branch '${submissions.comparedWith.branch}': this change touches no ` +
423
+ `recipe that declines proposals.`
424
+ );
425
+ } else if (submissions.comparedWith.kind === "tags") {
426
+ log(" Compared with each recipe's last release tag: nothing that declines proposals changed.");
427
+ } else if (submissions.comparedWith.kind === "none declined") {
428
+ log(" No recipe in this repository declines proposed changes.");
429
+ } else {
430
+ note(
431
+ "This checkout holds neither a copy of the default branch nor a release tag of any " +
432
+ "recipe that declines proposed changes, so there was nothing to compare with."
433
+ );
434
+ }
394
435
  footer();
395
436
  }
396
437