@sous-io/sous 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -35
- package/bin/run.js +10 -1
- package/docs/markdown/README.md +27 -0
- package/docs/markdown/_sidebar.md +18 -0
- package/docs/markdown/commands.md +308 -0
- package/docs/markdown/config-discovery.md +74 -0
- package/docs/markdown/config-inspection.md +69 -0
- package/docs/markdown/config-layers.md +92 -0
- package/docs/markdown/config-variables.md +79 -0
- package/docs/markdown/configuration.md +71 -0
- package/docs/markdown/design-principles.md +59 -0
- package/docs/markdown/repositories-authoring.md +408 -0
- package/docs/markdown/repositories-consuming.md +580 -0
- package/docs/markdown/repositories-file-formats.md +1084 -0
- package/docs/markdown/repositories-variables.md +387 -0
- package/docs/markdown/repositories.md +303 -0
- package/docs/markdown/skill-categories.md +58 -0
- package/package.json +72 -8
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
- package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
- package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
- package/sous.config.schema.json +337 -0
- package/src/base-command.ts +220 -67
- package/src/commands/build.ts +150 -73
- package/src/commands/clear.ts +23 -15
- package/src/commands/compile.ts +74 -16
- package/src/commands/config/get.ts +110 -0
- package/src/commands/config/show.ts +32 -0
- package/src/commands/config/validate.ts +53 -0
- package/src/commands/help.ts +46 -0
- package/src/commands/launch.ts +36 -14
- package/src/commands/lock/rebuild.ts +241 -0
- package/src/commands/lock/show.ts +115 -0
- package/src/commands/namespace/list.ts +117 -0
- package/src/commands/namespace/show.ts +110 -0
- package/src/commands/prune.ts +3 -11
- package/src/commands/recipe/list.ts +95 -0
- package/src/commands/recipe/show.ts +301 -0
- package/src/commands/repo/add.ts +145 -0
- package/src/commands/repo/gc.ts +172 -0
- package/src/commands/repo/init.ts +136 -0
- package/src/commands/repo/link.ts +500 -0
- package/src/commands/repo/list.ts +179 -0
- package/src/commands/repo/release.ts +619 -0
- package/src/commands/repo/remove.ts +193 -0
- package/src/commands/repo/search.ts +189 -0
- package/src/commands/repo/submit.ts +133 -0
- package/src/commands/repo/unlink.ts +147 -0
- package/src/commands/subscription/add.ts +285 -0
- package/src/commands/subscription/list.ts +129 -0
- package/src/commands/subscription/remove.ts +181 -0
- package/src/commands/vars/ask.ts +374 -0
- package/src/commands/vars/index.ts +79 -0
- package/src/commands/vars/list.ts +67 -0
- package/src/commands/vars/show.ts +77 -0
- package/src/config-command.ts +30 -0
- package/src/lib/build-service.ts +206 -54
- package/src/lib/config-discovery.ts +220 -27
- package/src/lib/config-inspect.ts +145 -0
- package/src/lib/config-kernel.mjs +377 -0
- package/src/lib/config-schema.ts +361 -0
- package/src/lib/env-file.ts +328 -0
- package/src/lib/env-local.ts +18 -1
- package/src/lib/errors.ts +32 -0
- package/src/lib/include-resolver.ts +108 -15
- package/src/lib/interactive.ts +165 -0
- package/src/lib/markdown-compiler.ts +118 -37
- package/src/lib/package-info.ts +25 -0
- package/src/lib/pid-service.ts +32 -21
- package/src/lib/refs/find.ts +589 -0
- package/src/lib/refs/index.ts +12 -0
- package/src/lib/refs/pick.ts +147 -0
- package/src/lib/refs/scopes.ts +61 -0
- package/src/lib/repos/catalog-display.ts +116 -0
- package/src/lib/repos/catalog-inputs.ts +160 -0
- package/src/lib/repos/catalog.ts +722 -0
- package/src/lib/repos/core-recipe.ts +105 -0
- package/src/lib/repos/defaults.ts +175 -0
- package/src/lib/repos/formats/common.ts +389 -0
- package/src/lib/repos/formats/index-file.ts +215 -0
- package/src/lib/repos/formats/links-map.ts +96 -0
- package/src/lib/repos/formats/lockfile.ts +167 -0
- package/src/lib/repos/formats/patterns.ts +57 -0
- package/src/lib/repos/formats/recipe-manifest.ts +395 -0
- package/src/lib/repos/formats/repo-manifest.ts +88 -0
- package/src/lib/repos/formats/store-entry.ts +84 -0
- package/src/lib/repos/freshness.ts +208 -0
- package/src/lib/repos/git-clone.ts +312 -0
- package/src/lib/repos/identity.ts +89 -0
- package/src/lib/repos/index.ts +58 -0
- package/src/lib/repos/links.ts +353 -0
- package/src/lib/repos/load-manifest.ts +236 -0
- package/src/lib/repos/lock-service.ts +453 -0
- package/src/lib/repos/locked-namespace-resolver.ts +90 -0
- package/src/lib/repos/locked-recipes.ts +254 -0
- package/src/lib/repos/managed-layer.ts +422 -0
- package/src/lib/repos/namespace-resolver.ts +370 -0
- package/src/lib/repos/providers/base.ts +206 -0
- package/src/lib/repos/providers/git.ts +233 -0
- package/src/lib/repos/providers/github.ts +294 -0
- package/src/lib/repos/providers/gitlab.ts +263 -0
- package/src/lib/repos/providers/http.ts +102 -0
- package/src/lib/repos/providers/index-cache.ts +382 -0
- package/src/lib/repos/providers/index.ts +106 -0
- package/src/lib/repos/providers/local.ts +391 -0
- package/src/lib/repos/providers/provider.ts +401 -0
- package/src/lib/repos/recipe-config-layers.ts +287 -0
- package/src/lib/repos/recipe-targets.ts +223 -0
- package/src/lib/repos/ref-search.ts +46 -0
- package/src/lib/repos/ref.ts +513 -0
- package/src/lib/repos/reference-report.ts +122 -0
- package/src/lib/repos/release/bump.ts +161 -0
- package/src/lib/repos/release/git-state.ts +305 -0
- package/src/lib/repos/release/index-builder.ts +635 -0
- package/src/lib/repos/release/index.ts +16 -0
- package/src/lib/repos/release/plan.ts +512 -0
- package/src/lib/repos/release/submit-service.ts +496 -0
- package/src/lib/repos/release/tags.ts +243 -0
- package/src/lib/repos/release/validate.ts +463 -0
- package/src/lib/repos/resolver.ts +789 -0
- package/src/lib/repos/scaffold/index.ts +238 -0
- package/src/lib/repos/scaffold/templates.ts +413 -0
- package/src/lib/repos/seed.ts +414 -0
- package/src/lib/repos/store/contract.ts +64 -0
- package/src/lib/repos/store/hash.ts +114 -0
- package/src/lib/repos/store/recipe-store.ts +599 -0
- package/src/lib/repos/store/settings.ts +58 -0
- package/src/lib/repos/subscription-service.ts +2678 -0
- package/src/lib/repos/trust.ts +447 -0
- package/src/lib/settings.ts +546 -189
- package/src/lib/sous-home.ts +104 -0
- package/src/lib/state.ts +52 -20
- package/src/lib/vars/ask.ts +1152 -0
- package/src/lib/vars/definition-source.ts +252 -0
- package/src/lib/vars/display.ts +233 -0
- package/src/lib/vars/index.ts +18 -0
- package/src/lib/vars/ladder.ts +282 -0
- package/src/lib/vars/mappings.ts +265 -0
- package/src/lib/vars/names.ts +94 -0
- package/src/lib/vars/preanswers.ts +395 -0
- package/src/lib/vars/question-plan.ts +218 -0
- package/src/lib/vars/report.ts +228 -0
- package/src/lib/vars/safe-regex.ts +235 -0
- package/src/lib/vars/validate.ts +312 -0
- package/src/lib/watch-loop.ts +148 -0
- package/src/templating/init-liquid-engine.ts +58 -16
- package/src/utils/choice-prompt.ts +143 -0
- package/src/utils/command-errors.ts +186 -0
- package/src/utils/command-help.ts +45 -0
- package/src/utils/confirm-prompt.ts +110 -0
- package/src/utils/flags.ts +153 -0
- package/src/utils/formatting.ts +540 -55
- package/src/utils/prompts.ts +35 -1
- package/src/utils/sous-directory.ts +245 -0
- package/src/utils/table.ts +603 -0
- package/src/utils/value-prompt.ts +119 -0
- package/shared-prompts/_partials/resume-task.md +0 -51
- package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
- package/shared-prompts/_partials/update-task-file.md +0 -52
- package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
- package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
- package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
- package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
- package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
- package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
- package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
- package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
- package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
- package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
- package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
- package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
- package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
- package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
- package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
- package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
- /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/references/liquid-filters.md +0 -0
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
# Repository File Formats
|
|
2
|
+
|
|
3
|
+
Repositories publish versioned **recipes**, grouped into **namespaces**, and projects subscribe
|
|
4
|
+
to them. Six files carry the whole system on disk. Three of them you write by hand; three sous
|
|
5
|
+
writes for you.
|
|
6
|
+
|
|
7
|
+
| File | Where | Written by | Purpose |
|
|
8
|
+
|------|-------|-----------|---------|
|
|
9
|
+
| `sous.repo.yaml` | repository root | you | Declares the repository: its namespaces and where its recipes live |
|
|
10
|
+
| `sous.recipe.yaml` | each recipe folder | you | Declares one recipe: version, dependencies, contents, variables |
|
|
11
|
+
| `sous.index.json` | repository root | `sous repo release` | Every namespace, recipe and published version, with content hashes |
|
|
12
|
+
| `sous.lock.json` | a project's `.sous/` | sous | What the project actually resolved to |
|
|
13
|
+
| `.sous.entry.json` | each store entry | sous | Makes a cached recipe version self-describing |
|
|
14
|
+
| `sous.links.json` | `.sous/` or `$SOUS_HOME` | `sous repo link` | Redirects a repository at a local working copy |
|
|
15
|
+
|
|
16
|
+
Every one of them carries `formatVersion: 1`. A future incompatible change bumps that number, so
|
|
17
|
+
an older sous refuses a file it would otherwise misread.
|
|
18
|
+
|
|
19
|
+
?> Hand-written manifests are YAML or JSON, never JavaScript. Repository trust rests on being
|
|
20
|
+
able to read a repository's whole surface without running any of its code, and a manifest that
|
|
21
|
+
could execute would break that guarantee.
|
|
22
|
+
|
|
23
|
+
## Refs: how anything is named
|
|
24
|
+
|
|
25
|
+
A **ref** names a namespace or a recipe. The same grammar works on the command line and in a
|
|
26
|
+
project's subscriptions. A recipe manifest names its dependencies by LOCATION instead, which is
|
|
27
|
+
a small variation on the same grammar; see [dependencies](#dependencies-named-by-location).
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
ref := [ repo ":" ] namespace [ "/" recipe [ "@" range ] ]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Ref | Means |
|
|
34
|
+
|-----|-------|
|
|
35
|
+
| `workflow` | The whole `workflow` namespace, including recipes published later |
|
|
36
|
+
| `workflow/task-files` | One recipe, any published version |
|
|
37
|
+
| `workflow/task-files@^1.2.0` | One recipe, constrained to a semantic version range |
|
|
38
|
+
| `sous-recipes:workflow/task-files@^1.2.0` | The same recipe, in a named repository |
|
|
39
|
+
|
|
40
|
+
The rules behind the grammar:
|
|
41
|
+
|
|
42
|
+
- **No prefix.** A ref is written bare. `@` introduces a version range and nothing else; `~` is
|
|
43
|
+
the template include sigil and never appears in a ref.
|
|
44
|
+
- **The repo qualifier is optional, and is yours.** Refs resolve across the cached indexes of
|
|
45
|
+
every added repository. You only need `repo:` when the same ref genuinely resolves in more than
|
|
46
|
+
one, and in that case sous reports the conflict and asks for the qualified form rather than
|
|
47
|
+
picking a winner. The short name is your project's own label for a repository, so it means
|
|
48
|
+
nothing in a published manifest and is refused there.
|
|
49
|
+
- **A version range applies to a recipe.** Namespaces are not versioned, so `workflow@^1.0.0` is
|
|
50
|
+
an error.
|
|
51
|
+
- **Ranges follow npm's rules.** `^1.2.0`, `~2.1`, `>=1.0.0 <2.0.0`, `1.x` and `*` all behave
|
|
52
|
+
exactly as they do in npm, because sous resolves them with npm's own `semver` package.
|
|
53
|
+
Prerelease versions sit out of range matching unless a subscription opts in.
|
|
54
|
+
|
|
55
|
+
Namespace names, recipe names and repository short names are lowercase kebab-case: a letter,
|
|
56
|
+
then letters, digits or hyphens.
|
|
57
|
+
|
|
58
|
+
## `sous.repo.yaml`: the repository manifest
|
|
59
|
+
|
|
60
|
+
The entry point sous reads to learn what a repository publishes. It lives at the repository root
|
|
61
|
+
and may be written as `sous.repo.yaml`, `sous.repo.yml` or `sous.repo.json`. Exactly one of
|
|
62
|
+
those, never two.
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
formatVersion: 1
|
|
66
|
+
|
|
67
|
+
# A suggested short name. A project records the name it actually uses when the
|
|
68
|
+
# repository is added, so two repositories suggesting the same name never collide.
|
|
69
|
+
name: sous-recipes
|
|
70
|
+
|
|
71
|
+
description: The official sous recipe repository.
|
|
72
|
+
|
|
73
|
+
# Surfaced when a provider cannot support `sous repo submit`, so a contributor is
|
|
74
|
+
# never left without a route. A URL or plain prose.
|
|
75
|
+
contribute: https://github.com/sous-io/sous-recipes/blob/main/CONTRIBUTING.md
|
|
76
|
+
|
|
77
|
+
namespaces:
|
|
78
|
+
core:
|
|
79
|
+
description: Skills that teach agents about sous itself.
|
|
80
|
+
workflow:
|
|
81
|
+
description: Task tracking and branch workflow.
|
|
82
|
+
|
|
83
|
+
# Each path holds a sous.recipe.yaml. Paths are relative to the repository root.
|
|
84
|
+
recipes:
|
|
85
|
+
- recipes/core/sous-skills
|
|
86
|
+
- recipes/workflow/task-files
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Field | Required | Type | Notes |
|
|
90
|
+
|-------|----------|------|-------|
|
|
91
|
+
| `formatVersion` | yes | `1` | The on-disk format version |
|
|
92
|
+
| `name` | yes | kebab-case string | Suggested short name for the repository |
|
|
93
|
+
| `description` | no | string | Shown by `sous repo list` and `sous repo search` |
|
|
94
|
+
| `contribute` | no | string | URL or prose describing how to contribute |
|
|
95
|
+
| `namespaces` | yes | map of name to `{ description? }` | Every namespace the repository publishes |
|
|
96
|
+
| `recipes` | yes | list of relative paths | Every recipe folder, each holding a recipe manifest |
|
|
97
|
+
|
|
98
|
+
Recipe paths stay inside the repository: an absolute path, a backslash, a `..` segment or a
|
|
99
|
+
trailing slash is rejected.
|
|
100
|
+
|
|
101
|
+
## `sous.recipe.yaml`: the recipe manifest
|
|
102
|
+
|
|
103
|
+
One recipe, complete. It lives in the recipe's own folder, as `sous.recipe.yaml`,
|
|
104
|
+
`sous.recipe.yml` or `sous.recipe.json`.
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
formatVersion: 1
|
|
108
|
+
namespace: workflow
|
|
109
|
+
name: task-files
|
|
110
|
+
version: 1.2.0
|
|
111
|
+
description: Per-branch task files, with skills for starting and resuming work.
|
|
112
|
+
|
|
113
|
+
# Build dependencies. Fetched, pinned and trust-gated, and addressable from this
|
|
114
|
+
# recipe's own files, but their files do NOT enter a subscriber's output.
|
|
115
|
+
# A bare ref names a recipe in this same repository.
|
|
116
|
+
depends:
|
|
117
|
+
- workflow/sat
|
|
118
|
+
|
|
119
|
+
# Co-subscriptions. Subscribing to this recipe subscribes the project to these too,
|
|
120
|
+
# with full semantics: their questions run and their files DO enter the output.
|
|
121
|
+
# A curated bundle is simply a recipe made mostly of these. A locator URL names a
|
|
122
|
+
# recipe in another repository.
|
|
123
|
+
subscribes:
|
|
124
|
+
- github://sous-io/sous-recipes/communication/control-flow@^2.0.0
|
|
125
|
+
|
|
126
|
+
# The files this recipe contributes. Patterns are relative to the recipe folder.
|
|
127
|
+
contents:
|
|
128
|
+
- kind: skills
|
|
129
|
+
include:
|
|
130
|
+
- skills/**/*.md
|
|
131
|
+
exclude:
|
|
132
|
+
- skills/**/draft-*.md
|
|
133
|
+
- kind: memories
|
|
134
|
+
include:
|
|
135
|
+
- memories/*.md
|
|
136
|
+
- kind: config
|
|
137
|
+
include:
|
|
138
|
+
- config/510-task-files.json
|
|
139
|
+
|
|
140
|
+
# Published specifications, not values. A question is asked only when a subscribed
|
|
141
|
+
# recipe needs the variable and no valid answer is already in scope.
|
|
142
|
+
variables:
|
|
143
|
+
- name: taskFileRoot
|
|
144
|
+
env: SOUS_VAR_TASK_FILE_ROOT
|
|
145
|
+
type: path
|
|
146
|
+
prompt: Where should task files live?
|
|
147
|
+
description: >-
|
|
148
|
+
One markdown file per git branch is written here. Most projects keep these
|
|
149
|
+
as local working notes and gitignore the directory.
|
|
150
|
+
example: .sous/tasks
|
|
151
|
+
default: .sous/tasks
|
|
152
|
+
required: true
|
|
153
|
+
scope: shared
|
|
154
|
+
|
|
155
|
+
- name: ticketSystem
|
|
156
|
+
type: enum
|
|
157
|
+
prompt: Which ticket system do you use?
|
|
158
|
+
description: >-
|
|
159
|
+
Decides which ticket identifiers the skills expect and which links they
|
|
160
|
+
write into a task file.
|
|
161
|
+
example: github
|
|
162
|
+
default: github
|
|
163
|
+
validate:
|
|
164
|
+
enum: [github, jira, linear]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Top-level fields
|
|
168
|
+
|
|
169
|
+
| Field | Required | Type | Notes |
|
|
170
|
+
|-------|----------|------|-------|
|
|
171
|
+
| `formatVersion` | yes | `1` | The on-disk format version |
|
|
172
|
+
| `namespace` | yes | kebab-case string | Must be declared by the repository manifest |
|
|
173
|
+
| `name` | yes | kebab-case string | Unique within its namespace |
|
|
174
|
+
| `version` | yes | exact semantic version | Never a range; `1.2.0`, or `2.0.0-beta.1` for a prerelease |
|
|
175
|
+
| `description` | no | string | Shown by `sous repo search` and `sous repo list` |
|
|
176
|
+
| `depends` | no | list of dependencies | Build dependencies, named by location |
|
|
177
|
+
| `subscribes` | no | list of dependencies | Co-subscriptions, named by location |
|
|
178
|
+
| `contents` | no | list of content groups | Defaults to an empty list, which is what a curated bundle wants |
|
|
179
|
+
| `variables` | no | list of variable definitions | Published specifications |
|
|
180
|
+
|
|
181
|
+
Recipe metadata is the source of truth for versions. A git tag shaped
|
|
182
|
+
`namespace/recipe@1.2.3` is a convenience ref that `sous repo release` keeps consistent with the
|
|
183
|
+
`version` field; a missing or wrong tag is reported rather than silently hiding a version.
|
|
184
|
+
|
|
185
|
+
### Dependencies named by location
|
|
186
|
+
|
|
187
|
+
`depends` and `subscribes` hold plain strings, and a manifest names its targets by WHERE THEY
|
|
188
|
+
LIVE. There are two spellings.
|
|
189
|
+
|
|
190
|
+
**A sibling**, in this same repository, is a bare ref:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
depends:
|
|
194
|
+
- workflow/sat # the sibling as released alongside me
|
|
195
|
+
- workflow/sat@^1.1 # supported, and uncommon
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
With no range, a sibling means "the version released alongside me": `sous repo release` cuts both
|
|
199
|
+
tags in one run and records the exact version in the index, so the pairing is fixed forever.
|
|
200
|
+
|
|
201
|
+
**Another repository** is a locator URL whose scheme is the provider's identifier:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
subscribes:
|
|
205
|
+
- github://sous-io/sous-recipes/workflow/sat@^1.1
|
|
206
|
+
- gitlab://gitlab.example.com/group/subgroup/project/workflow/sat
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The parsing rule is worth stating exactly, because it never guesses:
|
|
210
|
+
|
|
211
|
+
- The **last two path segments are always the namespace and the recipe**. That is the recipe's
|
|
212
|
+
published identity, never a path on disk: a recipe stored at `recipes/shared/sat/` and
|
|
213
|
+
published as `workflow/sat` is written `github://owner/repo/workflow/sat`. The repository's own
|
|
214
|
+
index maps that identity to the directory.
|
|
215
|
+
- **Everything before them is the repository.** A first segment carrying a dot is the host
|
|
216
|
+
(`gitlab.example.com`); otherwise the provider's own public host is used (`github.com`,
|
|
217
|
+
`gitlab.com`). Everything after the host is the repository's path, so GitLab subgroups work
|
|
218
|
+
without any extra syntax.
|
|
219
|
+
- The **range after `@` is optional**, and follows npm's rules like every other range.
|
|
220
|
+
|
|
221
|
+
Two things a manifest may not write:
|
|
222
|
+
|
|
223
|
+
- **`local://` is refused.** A local repository is a consumer's convenience for working on
|
|
224
|
+
recipes, not a published location; publish the recipe and depend on it where it lives.
|
|
225
|
+
- **`repo:` short names are refused.** A short name is the label one project chose when it added
|
|
226
|
+
a repository, and no other project has to agree with it.
|
|
227
|
+
|
|
228
|
+
A consumer matches a locator against the repositories it has added by their canonical identity
|
|
229
|
+
(`github.com/sous-io/sous-recipes`), so a project that added the same repository under a
|
|
230
|
+
different short name resolves against the copy it already has. One it has not added goes through
|
|
231
|
+
the ordinary trust round, which can show the URL the dependency named.
|
|
232
|
+
|
|
233
|
+
### Content groups
|
|
234
|
+
|
|
235
|
+
| Field | Required | Type | Notes |
|
|
236
|
+
|-------|----------|------|-------|
|
|
237
|
+
| `kind` | yes | `skills`, `memories`, `prompts` or `config` | Decides where the files land in a subscribing project |
|
|
238
|
+
| `include` | yes | list of glob patterns | At least one; relative to the recipe folder |
|
|
239
|
+
| `exclude` | no | list of glob patterns | Removed from the include set |
|
|
240
|
+
|
|
241
|
+
`config` entries name config layer files that are merged into the subscriber's config. Like
|
|
242
|
+
recipe paths, include and exclude patterns may not escape the recipe folder.
|
|
243
|
+
|
|
244
|
+
### Variable definitions
|
|
245
|
+
|
|
246
|
+
| Field | Required | Type | Notes |
|
|
247
|
+
|-------|----------|------|-------|
|
|
248
|
+
| `name` | yes | camelCase string | How templates refer to the variable |
|
|
249
|
+
| `env` | no | upper snake case string | The environment variable an answer binds to. `sous repo release` derives a default when it is omitted; the runtime never derives one |
|
|
250
|
+
| `type` | yes | `string`, `number`, `boolean`, `enum`, `path` or `url` | How the answer is validated and prompted for |
|
|
251
|
+
| `prompt` | yes | string | The one-line question text |
|
|
252
|
+
| `description` | yes | non-empty string | The paragraph explaining what the variable is for. Shown above the question when sous asks, and by `sous vars show <name>` |
|
|
253
|
+
| `example` | yes | string, number or boolean | A realistic sample answer, shown with the question. Checked against the declared type and the enum options exactly as `default` is, but never stored and never offered as the answer |
|
|
254
|
+
| `default` | no | string, number or boolean | Must match the declared type, and for an enum must be one of the options |
|
|
255
|
+
| `required` | no | boolean, default `true` | Whether a build needs an answer |
|
|
256
|
+
| `secret` | no | boolean, default `false` | A secret is always stored in the gitignored `.sous/.env.local` |
|
|
257
|
+
| `scope` | no | `shared` or `local`, default `shared` | Which env file the answer is written to |
|
|
258
|
+
| `validate` | no | object | Constraints, below |
|
|
259
|
+
|
|
260
|
+
A publisher has to explain every variable: `description` and `example` are both required, and a
|
|
261
|
+
manifest missing either is refused. The two are not interchangeable. An `example` is documentation
|
|
262
|
+
only, so it shows what a real answer looks like without sous ever storing it; a `default` is a real
|
|
263
|
+
value, offered as the answer when nothing else is in scope. A variable whose sample answer is
|
|
264
|
+
genuinely the right starting value carries the same text in both.
|
|
265
|
+
|
|
266
|
+
`scope: shared` writes to `.sous/.env`, which is committed and shared with the team.
|
|
267
|
+
`scope: local` writes to `.sous/.env.local`, which is gitignored and machine-specific. A secret
|
|
268
|
+
declared as shared is rejected, because that combination would commit the secret.
|
|
269
|
+
|
|
270
|
+
Constraints under `validate`:
|
|
271
|
+
|
|
272
|
+
| Field | Type | Applies to |
|
|
273
|
+
|-------|------|-----------|
|
|
274
|
+
| `pattern` | string holding a regular expression | String-like answers |
|
|
275
|
+
| `minLength`, `maxLength` | whole numbers | String-like answers |
|
|
276
|
+
| `min`, `max` | numbers | Numeric answers |
|
|
277
|
+
| `enum` | list of strings | Required when `type` is `enum` |
|
|
278
|
+
|
|
279
|
+
A `pattern` runs under a time budget, on a worker rather than on the thread waiting for the
|
|
280
|
+
answer; a pattern that exceeds the budget fails validation, and the failure names the pattern
|
|
281
|
+
rather than blaming the answer.
|
|
282
|
+
|
|
283
|
+
!> A schema may only LOOSEN within a major version. Tightening a constraint is a major bump,
|
|
284
|
+
and an upgrade re-validates stored answers, re-prompting only where an old answer no longer
|
|
285
|
+
fits.
|
|
286
|
+
|
|
287
|
+
### Unknown keys
|
|
288
|
+
|
|
289
|
+
Both hand-written manifests reject an unknown key and report it as a likely typo, naming the
|
|
290
|
+
file and the field's path. The one exception is the reserved `x-` namespace: a key such as
|
|
291
|
+
`x-team` is accepted and ignored, so a repository can carry metadata sous knows nothing about.
|
|
292
|
+
|
|
293
|
+
### JSON manifests
|
|
294
|
+
|
|
295
|
+
A `.json` manifest is read with a permissive parser: line comments, block comments and trailing
|
|
296
|
+
commas are all allowed, so a manifest can explain itself.
|
|
297
|
+
|
|
298
|
+
```json
|
|
299
|
+
{
|
|
300
|
+
// the only format version sous understands
|
|
301
|
+
"formatVersion": 1,
|
|
302
|
+
"namespace": "workflow",
|
|
303
|
+
"name": "task-files",
|
|
304
|
+
"version": "1.2.0",
|
|
305
|
+
"contents": [
|
|
306
|
+
{ "kind": "skills", "include": ["skills/**/*.md"] },
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## `sous.index.json`: the repository index
|
|
312
|
+
|
|
313
|
+
Machine-written by `sous repo release` and committed alongside the recipes it describes. The
|
|
314
|
+
index is the portable contract across providers: whatever a provider's API looks like, it can
|
|
315
|
+
hand back this one file, and it is all sous needs to resolve a ref, enumerate published versions
|
|
316
|
+
and check whether a cached copy is current.
|
|
317
|
+
|
|
318
|
+
Adding a repository fetches only this file. Nothing else is downloaded until a project
|
|
319
|
+
subscribes to something inside it.
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"formatVersion": 1,
|
|
324
|
+
"name": "sous-recipes",
|
|
325
|
+
"generatedAt": "2026-09-09T14:03:11.482Z",
|
|
326
|
+
"generator": "0.2.0",
|
|
327
|
+
"namespaces": {
|
|
328
|
+
"core": { "description": "Skills that teach agents about sous itself." },
|
|
329
|
+
"workflow": { "description": "Task tracking and branch workflow." }
|
|
330
|
+
},
|
|
331
|
+
"recipes": {
|
|
332
|
+
"workflow/task-files": {
|
|
333
|
+
"path": "recipes/workflow/task-files",
|
|
334
|
+
"description": "Per-branch task files.",
|
|
335
|
+
"versions": {
|
|
336
|
+
"1.0.0": {
|
|
337
|
+
"hash": "sha256-3b1f...c9",
|
|
338
|
+
"tag": "workflow/task-files@1.0.0",
|
|
339
|
+
"prerelease": false,
|
|
340
|
+
"releasedAt": "2026-08-01T09:00:00.000Z",
|
|
341
|
+
"dependencies": {
|
|
342
|
+
"workflow/sat": { "version": "1.4.0" },
|
|
343
|
+
"communication/control-flow": {
|
|
344
|
+
"repo": "github.com/sous-io/sous-recipes",
|
|
345
|
+
"range": "^2.0.0"
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"1.1.0-beta.1": {
|
|
350
|
+
"hash": "sha256-77ad...20",
|
|
351
|
+
"tag": "workflow/task-files@1.1.0-beta.1",
|
|
352
|
+
"prerelease": true
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
| Field | Required | Type | Notes |
|
|
361
|
+
|-------|----------|------|-------|
|
|
362
|
+
| `formatVersion` | yes | `1` | The on-disk format version |
|
|
363
|
+
| `name` | yes | kebab-case string | The repository's suggested short name |
|
|
364
|
+
| `generatedAt` | yes | ISO 8601 timestamp | When the index was generated |
|
|
365
|
+
| `generator` | yes | exact semantic version | The version of sous that generated it |
|
|
366
|
+
| `namespaces` | yes | map of name to `{ description? }` | Copied from the repository manifest |
|
|
367
|
+
| `recipes` | yes | map of `namespace/recipe` to a recipe entry | Every recipe published |
|
|
368
|
+
| `$comment` | no | string | A note about where this copy came from. JSON has no comment syntax, and an index is machine-written, so this is the one place a writer can say something to whoever opens the file. Sous ignores it, with one exception: the seed index below |
|
|
369
|
+
|
|
370
|
+
A recipe entry holds `path`, an optional `description`, and `versions`: a map from an exact
|
|
371
|
+
version to `{ hash, tag, prerelease, releasedAt?, dependencies?, seeded? }`. Every recipe needs
|
|
372
|
+
at least one version, and its namespace must be one the index declares. `seeded` is never
|
|
373
|
+
written by `sous repo release`; it marks the packaged core version sous folds in itself, and is
|
|
374
|
+
described under the seed index below.
|
|
375
|
+
|
|
376
|
+
### Resolved dependencies
|
|
377
|
+
|
|
378
|
+
`dependencies` records what one exact version was released against, keyed `namespace/recipe`. It
|
|
379
|
+
is why a published version means one thing forever: a consumer installing `1.0.0` installs the
|
|
380
|
+
versions `1.0.0` was published with, rather than re-resolving its ranges months later.
|
|
381
|
+
|
|
382
|
+
| Field | When | Notes |
|
|
383
|
+
|-------|------|-------|
|
|
384
|
+
| `version` | a sibling | The exact version, settled when both tags were cut |
|
|
385
|
+
| `repo` | another repository | That repository's canonical identity, `<host>/<owner path>/<name>` |
|
|
386
|
+
| `range` | another repository | The range the manifest declared; its exact version lives in that repository's own index |
|
|
387
|
+
|
|
388
|
+
The field is additive: an index written before it existed still parses, and a consumer that
|
|
389
|
+
finds no entry falls back to the ranges the recipe's manifest declares. `formatVersion` stays
|
|
390
|
+
`1`.
|
|
391
|
+
|
|
392
|
+
A version's `tag` must be exactly `<namespace>/<recipe>@<version>` for the entry it sits under,
|
|
393
|
+
and an index that says otherwise is refused when it is read. The tag is what the provider
|
|
394
|
+
fetches, so an index publishing `1.0.0` with `tag: "main"` would point a pinned version at a
|
|
395
|
+
branch: the content behind it changes on every push and the pinned hash then simply starts
|
|
396
|
+
failing, with nothing on the consumer's side able to say why.
|
|
397
|
+
|
|
398
|
+
Content hashes are written as `sha256-` followed by 64 lowercase hexadecimal characters. The
|
|
399
|
+
hash of a version is checked after every fetch, and against the lockfile before a cached copy is
|
|
400
|
+
used.
|
|
401
|
+
|
|
402
|
+
### The seed index
|
|
403
|
+
|
|
404
|
+
One index is not published by any repository: sous writes a stand-in index for its own
|
|
405
|
+
`sous-recipes` entry, into the index cache, when nothing real has ever been fetched from it. It
|
|
406
|
+
lists exactly one recipe, `core/sous-skills`, at the version of the running sous, with the hash
|
|
407
|
+
of the copy that was just seeded out of the installed package. That is what lets a project
|
|
408
|
+
resolve the core namespace on a machine that has never had a network connection.
|
|
409
|
+
|
|
410
|
+
The stand-in says so in its `$comment`, which is how a later run recognizes its own placeholder
|
|
411
|
+
and is willing to replace it; an index a repository actually published is never overwritten. No
|
|
412
|
+
freshness sidecar is written beside it, so the very first command that does have a network
|
|
413
|
+
fetches the real index rather than waiting out a window the stand-in never earned. When there is
|
|
414
|
+
still no network, sous reports that it could not check and uses the stand-in, which is the same
|
|
415
|
+
last-good behavior every repository gets.
|
|
416
|
+
|
|
417
|
+
### The packaged core version
|
|
418
|
+
|
|
419
|
+
The stand-in covers a machine that has never fetched anything. A machine that has been using
|
|
420
|
+
sous for a while holds the repository's real index instead, and that index publishes whatever
|
|
421
|
+
core versions the release pipeline has cut so far. Upgrade sous and the version the built-in
|
|
422
|
+
`core` subscription asks for is, for a while, not among them.
|
|
423
|
+
|
|
424
|
+
So sous folds the packaged version into that index IN MEMORY whenever the index does not carry
|
|
425
|
+
it: one more entry under `core/sous-skills`, at the running sous version, with the hash of the
|
|
426
|
+
copy just seeded out of the package, `prerelease` set from the version itself, and `seeded` set
|
|
427
|
+
to `true`. Nothing is written to disk; the cached file stays exactly what the repository served,
|
|
428
|
+
so the next real fetch is compared against the truth. The moment the repository does publish
|
|
429
|
+
that version, its own entry is what gets used, and if the two disagree about the content hash
|
|
430
|
+
sous says so once and prefers the published one.
|
|
431
|
+
|
|
432
|
+
## `sous.lock.json`: the project lockfile
|
|
433
|
+
|
|
434
|
+
Machine-written into the project's `.sous/` directory, and committed. The lockfile records the
|
|
435
|
+
exact version and content hash of everything the project currently uses, so a fresh clone
|
|
436
|
+
restores deterministically with no prompts and no version drift. Together with repository trust
|
|
437
|
+
it is the supply-chain defense: nothing new enters a project except through an explicit, visible
|
|
438
|
+
change to these files.
|
|
439
|
+
|
|
440
|
+
```json
|
|
441
|
+
{
|
|
442
|
+
"formatVersion": 1,
|
|
443
|
+
"repos": {
|
|
444
|
+
"sous-recipes": {
|
|
445
|
+
"url": "https://github.com/sous-io/sous-recipes",
|
|
446
|
+
"identity": "github.com/sous-io/sous-recipes",
|
|
447
|
+
"indexHash": "sha256-91cc...4e"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
"recipes": {
|
|
451
|
+
"core/sous-skills": {
|
|
452
|
+
"repo": "sous-recipes",
|
|
453
|
+
"version": "0.2.0",
|
|
454
|
+
"hash": "sha256-4d20...af",
|
|
455
|
+
"requestedBy": ["workflow/task-files"],
|
|
456
|
+
"kind": "depends"
|
|
457
|
+
},
|
|
458
|
+
"workflow/task-files": {
|
|
459
|
+
"repo": "sous-recipes",
|
|
460
|
+
"version": "1.2.0",
|
|
461
|
+
"hash": "sha256-3b1f...c9",
|
|
462
|
+
"requestedBy": ["project"],
|
|
463
|
+
"kind": "subscribes"
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| Field | Required | Type | Notes |
|
|
470
|
+
|-------|----------|------|-------|
|
|
471
|
+
| `formatVersion` | yes | `1` | The on-disk format version |
|
|
472
|
+
| `repos` | yes | map of short name to `{ url, identity, indexHash? }` | Every repository the locked recipes came from |
|
|
473
|
+
| `recipes` | yes | map of `namespace/recipe` to a locked entry | Everything currently in use |
|
|
474
|
+
|
|
475
|
+
A locked entry holds `repo` (which must appear under `repos`), the exact `version` resolved, its
|
|
476
|
+
`hash`, a `requestedBy` list and a `kind` of `subscribes` or `depends`.
|
|
477
|
+
|
|
478
|
+
A repository appears twice over, and the two are for different readers. The KEY is your
|
|
479
|
+
project's short name, which is what every message and every recipe entry names. The `identity`
|
|
480
|
+
is the canonical location it was derived from, and it is what the machine-wide store and the
|
|
481
|
+
index cache file that repository under, so two projects that call the same repository different
|
|
482
|
+
things still share one cached copy.
|
|
483
|
+
|
|
484
|
+
`requestedBy` is what makes removal safe. Every entry lists who holds it: the literal string
|
|
485
|
+
`project` for something the project subscribed to directly, or a recipe key for something pulled
|
|
486
|
+
in as a dependency. Unsubscribing removes one holder, and the entry itself goes only when the
|
|
487
|
+
last holder does.
|
|
488
|
+
|
|
489
|
+
Every key is written sorted, so a regenerated lockfile changes only when its content genuinely
|
|
490
|
+
does.
|
|
491
|
+
|
|
492
|
+
## `.sous.entry.json`: the store entry marker
|
|
493
|
+
|
|
494
|
+
The machine-wide store lives under the user-level sous directory, one folder per recipe version:
|
|
495
|
+
`$SOUS_HOME/cache/<repository identity>/<namespace>/<recipe>/<version>/`. A marker beside each
|
|
496
|
+
one makes the entry self-describing, so the store can be verified and swept without consulting
|
|
497
|
+
any project.
|
|
498
|
+
|
|
499
|
+
```json
|
|
500
|
+
{
|
|
501
|
+
"formatVersion": 1,
|
|
502
|
+
"repo": "github.com/sous-io/sous-recipes",
|
|
503
|
+
"namespace": "workflow",
|
|
504
|
+
"name": "task-files",
|
|
505
|
+
"version": "1.2.0",
|
|
506
|
+
"hash": "sha256-3b1f...c9",
|
|
507
|
+
"fetchedAt": "2026-09-01T10:00:00.000Z",
|
|
508
|
+
"lastAccessAt": "2026-09-09T14:03:11.482Z",
|
|
509
|
+
"sizeBytes": 20480
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Every field is required. `repo` is the repository's canonical identity, because the store is
|
|
514
|
+
shared by every project on the machine and a short name is one project's private label. `hash`
|
|
515
|
+
is checked against the lockfile before the entry is used; `sizeBytes` and `lastAccessAt` drive
|
|
516
|
+
the size-capped, least-recently-used collection that `sous repo gc` performs.
|
|
517
|
+
|
|
518
|
+
The store is disposable by design: everything in it is re-fetchable from the pins in a project's
|
|
519
|
+
lockfile. Builds read inputs from the store and render or copy outputs into the project; sous
|
|
520
|
+
does not symlink store content into a project, and store content is never edited in place.
|
|
521
|
+
|
|
522
|
+
## The store on disk
|
|
523
|
+
|
|
524
|
+
The store is a plain directory tree under the user-level sous directory, which is `~/.sous`
|
|
525
|
+
unless `SOUS_HOME` says otherwise. Unlike `SOUS_CONFIG` and `SOUS_DIR`, `SOUS_HOME` does not
|
|
526
|
+
decide which project is active, so it may be set in an env file (`.sous/.env.local` or
|
|
527
|
+
`.sous/.env`) as well as in the shell. The same directory holds globally linked checkouts
|
|
528
|
+
(`repos/`) and the machine-wide links map.
|
|
529
|
+
|
|
530
|
+
```text
|
|
531
|
+
$SOUS_HOME/
|
|
532
|
+
cache/ the store root
|
|
533
|
+
_indexes/<identity>.json one cached index per repository
|
|
534
|
+
<identity>/<namespace>/<recipe>/<version>/
|
|
535
|
+
.sous.entry.json the marker for this entry
|
|
536
|
+
... the recipe's files, exactly as fetched
|
|
537
|
+
repos/<owner>/<repo>/ checkouts linked with --global
|
|
538
|
+
sous.links.json the machine-wide links map
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`<identity>` is the repository's canonical location, `<host>/<owner path>/<name>`, so it is
|
|
542
|
+
several directories deep on its own: `github.com/sous-io/sous-recipes/`, with the namespace, the
|
|
543
|
+
recipe and the version following it. Keying by location rather than by
|
|
544
|
+
a short name is what lets two projects that call a repository different things share one cached
|
|
545
|
+
copy, and stops two projects that use the same short name for different repositories from
|
|
546
|
+
colliding. Nothing migrates a store keyed another way: entries sous cannot find are simply
|
|
547
|
+
fetched again, which costs a download and nothing else.
|
|
548
|
+
|
|
549
|
+
An entry is written atomically: sous copies the fetched files into a temporary directory
|
|
550
|
+
beside the entry's final home, hashes them, checks the hash against the pin it was given,
|
|
551
|
+
writes the marker, and only then renames the directory into place. A published version is
|
|
552
|
+
immutable, so re-storing one is allowed only when the content hashes the same; different
|
|
553
|
+
content under a version already in the store is an error rather than a silent overwrite.
|
|
554
|
+
|
|
555
|
+
The content hash is SHA-256 over a canonical serialization of the folder: files in bytewise
|
|
556
|
+
order of their relative paths, each contributing its path, its byte length and its bytes.
|
|
557
|
+
File modes, owners and timestamps are excluded, so the same recipe hashes the same after a
|
|
558
|
+
copy, a clone or an archive round-trip. `.git` and the entry's own `.sous.entry.json` are
|
|
559
|
+
skipped, which is what lets sous touch the marker without invalidating the entry. Every read
|
|
560
|
+
re-verifies the hash; an entry that no longer matches is removed and re-fetched, and the
|
|
561
|
+
build says so.
|
|
562
|
+
|
|
563
|
+
Symbolic links are skipped entirely, by the hash and by the copy into the store alike, and so
|
|
564
|
+
is anything under a linked directory. A link points at bytes the repository does not own, so
|
|
565
|
+
following one would make the same published version hash differently on the publisher's
|
|
566
|
+
machine and the consumer's, and every install would then fail against its own pin.
|
|
567
|
+
`sous repo release` refuses to publish a recipe folder containing a link, naming it, so this
|
|
568
|
+
is caught where it can be fixed rather than at install time. A recipe that needs a file ships
|
|
569
|
+
the file.
|
|
570
|
+
|
|
571
|
+
Collection is size-capped and least-recently-used. `store.maxBytes` sets the cap (one
|
|
572
|
+
gigabyte by default) and `lastAccessAt` in each marker sets the order; entries a lockfile
|
|
573
|
+
still pins are never evicted, even when honoring the cap would require it. Everything in
|
|
574
|
+
the store is re-fetchable from those pins, so deleting the whole directory costs a download
|
|
575
|
+
and nothing else.
|
|
576
|
+
|
|
577
|
+
## `sous.links.json`: the links map
|
|
578
|
+
|
|
579
|
+
A link redirects a repository's resolution away from the store and at a real working copy, which
|
|
580
|
+
is how a maintainer edits recipes: edits happen in a checkout, never in the store.
|
|
581
|
+
|
|
582
|
+
```json
|
|
583
|
+
{
|
|
584
|
+
"formatVersion": 1,
|
|
585
|
+
"links": {
|
|
586
|
+
"sous-recipes": {
|
|
587
|
+
"path": "/home/me/Projects/sous-recipes",
|
|
588
|
+
"linkedAt": "2026-09-09T14:03:11.482Z",
|
|
589
|
+
"origin": "clone"
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
| Field | Required | Type | Notes |
|
|
596
|
+
|-------|----------|------|-------|
|
|
597
|
+
| `formatVersion` | yes | `1` | The on-disk format version |
|
|
598
|
+
| `links` | yes | map of repository short name to a link entry | Everything currently linked |
|
|
599
|
+
|
|
600
|
+
A link entry holds an absolute `path`, a `linkedAt` timestamp, and an `origin` of `clone` (sous
|
|
601
|
+
cloned the working copy itself) or `path` (sous was pointed at an existing checkout). Unlinking
|
|
602
|
+
removes the entry and leaves the checkout on disk.
|
|
603
|
+
|
|
604
|
+
Two maps are read: the project's `.sous/sous.links.json` and the machine-wide
|
|
605
|
+
`$SOUS_HOME/sous.links.json`, with the project map winning on conflict. The file is never
|
|
606
|
+
committed. A link bypasses versions, the lockfile and freshness checks, and those bypasses
|
|
607
|
+
belong to one person's machine rather than to the team, so builds announce a linked repository
|
|
608
|
+
loudly.
|
|
609
|
+
|
|
610
|
+
### Where a linked checkout lives
|
|
611
|
+
|
|
612
|
+
`sous repo link <repo>` with no path clones the repository for you. The working copy lands in
|
|
613
|
+
`.sous/repos/<owner>/<name>`, or in `$SOUS_HOME/repos/<owner>/<name>` with `--global`, where
|
|
614
|
+
every project on the machine shares one checkout. A directory that is already a checkout of the
|
|
615
|
+
same remote is reused rather than cloned again, so running the command twice is harmless; one
|
|
616
|
+
holding a different remote is an error, because reading the wrong recipes silently would be
|
|
617
|
+
worse than stopping.
|
|
618
|
+
|
|
619
|
+
`sous repo link <repo> <path>` links a checkout that already exists and clones nothing. The path
|
|
620
|
+
must hold a repo manifest at its root, since a directory without one is not a repository.
|
|
621
|
+
|
|
622
|
+
`<repo>` is normally the short name of a repository this project has already added. A URL is
|
|
623
|
+
accepted, but it is not a way around adding one: a linked repository's recipes are read with no
|
|
624
|
+
version, no lockfile and no hash check, so a URL the project has not added runs the same trust
|
|
625
|
+
ceremony `sous repo add` runs before anything is cloned or linked. It asks inline, `--trust`
|
|
626
|
+
acknowledges instead for a run with no terminal, and a URL whose short name already belongs to a
|
|
627
|
+
different repository is refused outright.
|
|
628
|
+
|
|
629
|
+
`sous repo unlink <repo>` removes the map entry and nothing else. The checkout stays where it
|
|
630
|
+
is, and its path is printed so you can delete it yourself if you want to.
|
|
631
|
+
|
|
632
|
+
### Ignore hygiene
|
|
633
|
+
|
|
634
|
+
Everything sous keeps under `.sous/` for one machine is kept out of the project's repository,
|
|
635
|
+
and linking maintains both files that do it:
|
|
636
|
+
|
|
637
|
+
- `.sous/repos/.gitignore` holds a single `*`. That covers the ignore file itself, so a cloned
|
|
638
|
+
checkout underneath it contributes nothing at all to the project's repository.
|
|
639
|
+
- `.sous/.gitignore` carries a delimited managed block:
|
|
640
|
+
|
|
641
|
+
```
|
|
642
|
+
# >>> sous managed (do not edit between these markers)
|
|
643
|
+
sous.links.json
|
|
644
|
+
sous.state.json
|
|
645
|
+
sous.pid
|
|
646
|
+
repos/
|
|
647
|
+
# <<< sous managed
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Only the lines between the markers are ever rewritten. Anything you put above or below them is
|
|
651
|
+
left exactly as it was, and both files are written only when their contents would change, so
|
|
652
|
+
linking repeatedly never produces a diff. An opening marker with no closing partner stops the
|
|
653
|
+
command with an error rather than a guess about where the block ends.
|
|
654
|
+
|
|
655
|
+
`sous prune` and `sous clear` only ever touch paths recorded in the state file, so nothing in
|
|
656
|
+
`.sous/repos/` is at risk from either of them.
|
|
657
|
+
|
|
658
|
+
## Project configuration
|
|
659
|
+
|
|
660
|
+
Four optional top-level keys in a project's sous config carry the consumer side. `sous repo add`
|
|
661
|
+
and `sous subscription add` write the first two into machine-managed `conf.d/` layers, and you may also
|
|
662
|
+
hand-write them in the primary config; the layers merge like anything else. The fourth,
|
|
663
|
+
`recipeOutputs`, is always yours to write and is covered under
|
|
664
|
+
[Consuming recipes](#consuming-recipes).
|
|
665
|
+
|
|
666
|
+
```yaml
|
|
667
|
+
# Trusted repositories, keyed by the short name refs use. Adding a repository IS
|
|
668
|
+
# trusting it, and removing the entry withdraws that trust.
|
|
669
|
+
repos:
|
|
670
|
+
team-recipes:
|
|
671
|
+
url: https://github.com/example-org/team-recipes
|
|
672
|
+
provider: github # inferred from the URL when omitted
|
|
673
|
+
enabled: true # defaults to true; false takes it out of play entirely
|
|
674
|
+
alwaysPull: false
|
|
675
|
+
addedAt: 2026-09-09T14:03:11.482Z
|
|
676
|
+
addedBy: user # "user", "sous", or the ref of the recipe that required it
|
|
677
|
+
|
|
678
|
+
# What the project subscribes to, keyed by ref key.
|
|
679
|
+
subscriptions:
|
|
680
|
+
workflow/task-files:
|
|
681
|
+
range: ^1.2.0
|
|
682
|
+
enabled: true # defaults to true
|
|
683
|
+
prerelease: false
|
|
684
|
+
alwaysPull: false
|
|
685
|
+
|
|
686
|
+
# Knobs for the machine-wide store. Every number here is configurable; the values
|
|
687
|
+
# sous ships are defaults, not assumptions.
|
|
688
|
+
store:
|
|
689
|
+
maxBytes: 1073741824 # one gigabyte
|
|
690
|
+
freshnessSeconds: 300 # five minutes
|
|
691
|
+
watchPollSeconds: 300 # five minutes
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
A subscription key is a ref key: a namespace, or a namespace and a recipe. It never carries a
|
|
695
|
+
repository qualifier or a version range, because the range belongs in the entry.
|
|
696
|
+
|
|
697
|
+
### The entries sous provides itself
|
|
698
|
+
|
|
699
|
+
Two entries are there without you writing them. Sous lays them UNDER whatever your config
|
|
700
|
+
layers produced, so `sous config show` prints them and `sous repo list` marks the repository
|
|
701
|
+
"built in":
|
|
702
|
+
|
|
703
|
+
- the repository `sous-recipes`, pointing at `https://github.com/sous-io/sous-recipes`, and
|
|
704
|
+
- a `core` subscription, whose range is exactly the version of sous you are running.
|
|
705
|
+
|
|
706
|
+
The `core` namespace holds the skills that teach an agent what sous is and how it works, and a
|
|
707
|
+
copy of it ships inside the sous package, so a brand new project builds with those skills even
|
|
708
|
+
with no network. Trust is not a question here: sous itself ships the recipe and pins the
|
|
709
|
+
version to its own.
|
|
710
|
+
|
|
711
|
+
The range being the exact running version is deliberate. Core is published in lockstep with the
|
|
712
|
+
CLI, so upgrading sous upgrades core with it, and a build re-pins core the first time it notices
|
|
713
|
+
that the locked version no longer satisfies the range.
|
|
714
|
+
|
|
715
|
+
### Switching either one off
|
|
716
|
+
|
|
717
|
+
`enabled` is an ordinary field on any `repos` or `subscriptions` entry. It defaults to true, and
|
|
718
|
+
setting it to false takes that entry out of play entirely: nothing resolves through it, nothing
|
|
719
|
+
is fetched for it, and nothing it publishes is compiled. The entry stays in your config, so the
|
|
720
|
+
opt-out is legible to whoever reads it next.
|
|
721
|
+
|
|
722
|
+
The two entries above are what it is mostly for:
|
|
723
|
+
|
|
724
|
+
```yaml
|
|
725
|
+
# Keep the repository, but do not install the core skills.
|
|
726
|
+
subscriptions:
|
|
727
|
+
core:
|
|
728
|
+
enabled: false
|
|
729
|
+
|
|
730
|
+
# Or drop the repository entirely, which withdraws the core subscription with it.
|
|
731
|
+
repos:
|
|
732
|
+
sous-recipes:
|
|
733
|
+
enabled: false
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
Those two lines are complete entries on their own. Sous merges your fields over its own field by
|
|
737
|
+
field, so `{ enabled: false }` inherits the URL and provider underneath it; writing a `url`
|
|
738
|
+
repoints the repository and changes nothing else.
|
|
739
|
+
|
|
740
|
+
`alwaysPull` installs a newer in-range version whenever one exists rather than holding the
|
|
741
|
+
locked one. It never widens the range a subscription or a dependency declared, and the lockfile
|
|
742
|
+
is still regenerated continuously so it records what the last build actually used. A freshness
|
|
743
|
+
check that fails never breaks a build; the last good answer stands.
|
|
744
|
+
|
|
745
|
+
## Managed config layers
|
|
746
|
+
|
|
747
|
+
Sous writes three of those keys itself, into the `conf.d/` band reserved for machine-written
|
|
748
|
+
layers:
|
|
749
|
+
|
|
750
|
+
| File | Holds | Written by |
|
|
751
|
+
|------|-------|------------|
|
|
752
|
+
| `conf.d/500-repos.jsonc` | the `repos:` map | `sous repo add` |
|
|
753
|
+
| `conf.d/510-subscriptions.jsonc` | the `subscriptions:` map | `sous subscription add`, `sous subscription remove` |
|
|
754
|
+
| `conf.d/520-var-mappings.jsonc` | the `varMappings:` map | `sous vars ask` |
|
|
755
|
+
|
|
756
|
+
All three are ordinary config layers: they load in filename order after your primary config and
|
|
757
|
+
merge into it, so a repository you hand-write in your own config and one sous added are the same
|
|
758
|
+
thing by the time anything reads them. Sous never edits your primary config, and never edits a
|
|
759
|
+
layer outside the `500` through `599` band.
|
|
760
|
+
|
|
761
|
+
**Sous edits these files by key; you may edit them too.** An edit rewrites only the bytes of the
|
|
762
|
+
entry that changes, through a JSON-with-comments editor, so your comments, your key order and your
|
|
763
|
+
formatting all survive it. New entries are inserted in sorted order, so a change to one repository
|
|
764
|
+
shows up as a change to one repository in your version control history.
|
|
765
|
+
|
|
766
|
+
They are `.jsonc`, not `.json`, which is why each one can open with a header comment saying what it
|
|
767
|
+
holds:
|
|
768
|
+
|
|
769
|
+
```jsonc
|
|
770
|
+
// This file is managed by sous. Sous edits these files by key; you may edit
|
|
771
|
+
// them too, and your comments, key order and formatting are kept.
|
|
772
|
+
//
|
|
773
|
+
// It records the repositories this project trusts. The 'sous repo add'
|
|
774
|
+
// command writes the entries under 'repos'.
|
|
775
|
+
//
|
|
776
|
+
// It is JSON with comments (.jsonc): line comments, block comments and trailing
|
|
777
|
+
// commas are all allowed here.
|
|
778
|
+
{
|
|
779
|
+
"repos": {
|
|
780
|
+
"sous-recipes": {
|
|
781
|
+
"url": "https://github.com/sous-io/sous-recipes",
|
|
782
|
+
"addedAt": "2026-09-09T14:03:11.482Z",
|
|
783
|
+
// ours; the whole team reads from it
|
|
784
|
+
"addedBy": "user"
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
Sous reads `.jsonc` anywhere it reads `.json`: a primary `sous.config.jsonc`, any `conf.d/` layer, a
|
|
791
|
+
repo or recipe manifest, and a config layer a recipe contributes. A layer still named
|
|
792
|
+
`500-repos.json` from an older sous is read as a fallback, and the next write moves it to `.jsonc`
|
|
793
|
+
and removes the old file. Only one of the two names may exist at a time; two layers whose names
|
|
794
|
+
differ only by extension are a hard error.
|
|
795
|
+
|
|
796
|
+
Where a comment is impossible because the format really is strict JSON, the convention is a `//`
|
|
797
|
+
key, which sous ignores wherever it appears. Nothing sous writes uses one today.
|
|
798
|
+
|
|
799
|
+
The machine-written files that are NOT config layers stay strict JSON, because they carry no prose
|
|
800
|
+
and other tooling parses them: the lockfile, the repo index, the links map and the marker beside a
|
|
801
|
+
store entry.
|
|
802
|
+
|
|
803
|
+
## Consuming recipes
|
|
804
|
+
|
|
805
|
+
Subscribing pins a recipe; building is what turns it into files in your project. A recipe's
|
|
806
|
+
`contents` block says what it contributes and of what kind, and each kind lands somewhere your
|
|
807
|
+
config decides.
|
|
808
|
+
|
|
809
|
+
### `recipeOutputs`: where the files land
|
|
810
|
+
|
|
811
|
+
```yaml
|
|
812
|
+
# Where the files subscribed recipes contribute are written, per content kind.
|
|
813
|
+
# Every path is ${var} substituted like any other config path, and a kind may
|
|
814
|
+
# name several destinations so one recipe feeds more than one agent directory.
|
|
815
|
+
recipeOutputs:
|
|
816
|
+
skills:
|
|
817
|
+
- ${projectRoot}/.claude/skills
|
|
818
|
+
- ${projectRoot}/.codex/skills
|
|
819
|
+
memories:
|
|
820
|
+
- ${projectRoot}/.claude/memories
|
|
821
|
+
prompts:
|
|
822
|
+
- ${projectRoot}/prompts/recipes
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
Only `skills` has a default: `<project root>/.claude/skills`, the project root being the parent
|
|
826
|
+
of your `.sous/` directory. That is where every agent looks, so it is worth defaulting. Nothing
|
|
827
|
+
else is: a kind with no destination is skipped, and sous says so once, naming this key. It will
|
|
828
|
+
not guess where you want your memories or your prompts.
|
|
829
|
+
|
|
830
|
+
Files are written the same way an `entryGlob` target of your own writes them. The static part of
|
|
831
|
+
each include pattern is the base the output tree mirrors, so a recipe publishing
|
|
832
|
+
`skills/task-files/SKILL.md` under `include: ["skills/**/*.md"]` writes
|
|
833
|
+
`<destination>/task-files/SKILL.md`. The [`.tpl.` convention](configuration.md) applies
|
|
834
|
+
unchanged: a `.tpl.md` file is rendered and loses the `.tpl.` from its name, and everything else
|
|
835
|
+
is copied verbatim.
|
|
836
|
+
|
|
837
|
+
?> Only recipes you are subscribed to contribute files. A recipe pulled in through `depends` is
|
|
838
|
+
fetched, pinned and addressable from the recipe that declared it, and its files never enter your
|
|
839
|
+
output. That is the whole difference between the two dependency kinds.
|
|
840
|
+
|
|
841
|
+
Recipe outputs are tracked like every other file sous writes, so `sous prune` removes what an
|
|
842
|
+
unsubscribed recipe used to write, and `sous clear` removes all of it. Neither ever reaches into
|
|
843
|
+
a linked checkout or the machine-wide store; both hold work that is not a project's to delete.
|
|
844
|
+
|
|
845
|
+
### `config` contents: recipes that configure
|
|
846
|
+
|
|
847
|
+
A recipe's `config` contents are not written anywhere. They are config layers, and they load
|
|
848
|
+
after your primary config and before your own `conf.d/` layers, so a recipe can supply defaults
|
|
849
|
+
and your project always wins over them.
|
|
850
|
+
|
|
851
|
+
A recipe's config layer is JSON or YAML only (`.json`, `.yaml` or `.yml`). Sous must be able to
|
|
852
|
+
read everything a repository publishes without running any of it, so an executable layer from a
|
|
853
|
+
recipe is refused with a warning rather than loaded, exactly as manifests are.
|
|
854
|
+
|
|
855
|
+
A recipe's config layer may set only these top-level keys:
|
|
856
|
+
|
|
857
|
+
| Key | What a recipe uses it for |
|
|
858
|
+
|-----|---------------------------|
|
|
859
|
+
| `_vars` | Default values for the variables its templates read |
|
|
860
|
+
| `_aliases` | Include aliases pointing at the files it ships |
|
|
861
|
+
| `compilation` | Targets that compile what it ships |
|
|
862
|
+
| `runtimeContext` | Runtime context for the templates it ships |
|
|
863
|
+
| `recipeOutputs` | Where the files it contributes are written |
|
|
864
|
+
| `store` | Knobs for the machine-wide store |
|
|
865
|
+
| `varMappings` | Bindings from an environment variable name to one of its variables |
|
|
866
|
+
|
|
867
|
+
Every other key is removed before the layer is merged, and sous prints a warning naming the
|
|
868
|
+
recipe and the key it removed. In particular a recipe may not set `repos`, `subscriptions`,
|
|
869
|
+
`tools`, `_env`, `version`, `name` or `$schema`, and it may not set a key sous does not
|
|
870
|
+
recognise. Subscribing to a recipe is not a decision to let it choose which repositories this
|
|
871
|
+
project trusts, what else it subscribes to, or which programs `sous launch` runs; those stay
|
|
872
|
+
yours. Sous reads the layer itself and applies this filter before the config kernel merges
|
|
873
|
+
anything, so the kernel never opens a recipe's layer file at all.
|
|
874
|
+
|
|
875
|
+
### The `local` provider: repositories on this machine
|
|
876
|
+
|
|
877
|
+
A repository does not have to be hosted. Name one by a path, relative or absolute, or by the
|
|
878
|
+
same path in `file:///...` form, and sous reads it through the built-in `local` provider:
|
|
879
|
+
|
|
880
|
+
```bash
|
|
881
|
+
sous repo add /home/me/Projects/my-recipes --name my-recipes
|
|
882
|
+
sous repo add ../my-recipes --name my-recipes
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
A relative path is resolved against the working directory and stored in its absolute form, since
|
|
886
|
+
a repository on this machine is machine-specific anyway.
|
|
887
|
+
|
|
888
|
+
It is meant for local development and for tests: authoring a repository, trying a recipe before
|
|
889
|
+
publishing it, or running a whole workflow with no network at all. The index is read from the
|
|
890
|
+
working tree when the file is there, so an index you are still writing is picked up without a
|
|
891
|
+
commit, and from the committed copy otherwise. A recipe's files come from the version's tag in
|
|
892
|
+
the local git repository; a directory that is not a git repository has no versions to honour, so
|
|
893
|
+
its working tree is copied instead.
|
|
894
|
+
|
|
895
|
+
!> Trust semantics are identical to a hosted repository. A local path is added, and therefore
|
|
896
|
+
trusted, through the same ceremony, because the recipes in it still run on this machine. "It is
|
|
897
|
+
already on my disk" is not a reason to skip the question.
|
|
898
|
+
|
|
899
|
+
For editing a repository you are already subscribed to, reach for `sous repo link` instead: it
|
|
900
|
+
redirects one repository's resolution at a working copy without changing what your project
|
|
901
|
+
subscribes to.
|
|
902
|
+
|
|
903
|
+
## Variables and answers
|
|
904
|
+
|
|
905
|
+
A variable **definition** is a published specification; an **answer** is the stored value. A
|
|
906
|
+
question is asked only when a subscribed recipe needs a variable and nothing in scope answers
|
|
907
|
+
it, or when the answer in scope no longer fits the definition.
|
|
908
|
+
|
|
909
|
+
### Where answers live
|
|
910
|
+
|
|
911
|
+
Answers are stored in the project's own env files, and sous edits them the way a careful person
|
|
912
|
+
would: exactly one value line is rewritten or appended, and comments, blank lines, ordering and
|
|
913
|
+
quoting all survive. A newly added entry gets a short generated header comment above it saying
|
|
914
|
+
where the value came from. Comments are output only; sous never reads one back.
|
|
915
|
+
|
|
916
|
+
| File | Committed | Holds |
|
|
917
|
+
|------|-----------|-------|
|
|
918
|
+
| `.sous/.env` | yes | Shared answers (`scope: shared`), the team's defaults |
|
|
919
|
+
| `.sous/.env.local` | no, gitignored | Machine-specific answers (`scope: local`) and every secret |
|
|
920
|
+
|
|
921
|
+
### The resolution ladder
|
|
922
|
+
|
|
923
|
+
For each variable sous generates a list of environment variable names and tries them in order,
|
|
924
|
+
most specific first. Within a rung, the real shell environment wins, then `.sous/.env.local`,
|
|
925
|
+
then `.sous/.env`.
|
|
926
|
+
|
|
927
|
+
| Rung | Name | Example |
|
|
928
|
+
|------|------|---------|
|
|
929
|
+
| 1. mapping record | whatever the record names | `TEAM_API_URL` |
|
|
930
|
+
| 2. recipe scope | `SOUS_VAR_<NAMESPACE>_<RECIPE>_<VARIABLE>` | `SOUS_VAR_MISC_STUFF_API_URL` |
|
|
931
|
+
| 3. namespace scope | `SOUS_VAR_<NAMESPACE>_<VARIABLE>` | `SOUS_VAR_MISC_API_URL` |
|
|
932
|
+
| 4. shared scope | `SOUS_VAR_<VARIABLE>` | `SOUS_VAR_API_URL` |
|
|
933
|
+
| 5. declared name | the definition's own `env` field | `GITHUB_TOKEN` |
|
|
934
|
+
|
|
935
|
+
!> Candidate names are only ever GENERATED and looked up, never parsed back into scopes. The
|
|
936
|
+
underscore is both the delimiter and a legal identifier character, so no parse of a name would
|
|
937
|
+
be trustworthy. When names collide, a mapping record settles it.
|
|
938
|
+
|
|
939
|
+
### Mapping records
|
|
940
|
+
|
|
941
|
+
A mapping record binds one environment variable, of any name, to one fully qualified variable.
|
|
942
|
+
It is the top rung of the ladder and the universal conflict resolver: two recipes wanting the
|
|
943
|
+
same name, or a name already meaning something else in your environment.
|
|
944
|
+
|
|
945
|
+
```jsonc
|
|
946
|
+
// This file is managed by sous. Sous edits these files by key; you may edit
|
|
947
|
+
// them too, and your comments, key order and formatting are kept.
|
|
948
|
+
{
|
|
949
|
+
"varMappings": {
|
|
950
|
+
"TEAM_API_URL": "sous-recipes:misc/stuff/apiUrl"
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
A target is written `namespace/recipe/variableName`, optionally qualified as
|
|
956
|
+
`repo:namespace/recipe/variableName`. Records live under the top-level `varMappings` config key;
|
|
957
|
+
sous writes the ones it creates into the machine-managed `conf.d/520-var-mappings.jsonc` layer,
|
|
958
|
+
editing one record at a time so each name has exactly one, and you may hand-write `varMappings` in
|
|
959
|
+
the primary config too.
|
|
960
|
+
|
|
961
|
+
### The commands
|
|
962
|
+
|
|
963
|
+
| Command | What it does |
|
|
964
|
+
|---------|--------------|
|
|
965
|
+
| `sous vars list` | Lists every variable in play: its recipe, the environment variable that answered it, the value (hidden for a secret) and the source |
|
|
966
|
+
| `sous vars show <name>` | Shows one variable in full, including every environment variable on the ladder and which rung answered |
|
|
967
|
+
| `sous vars ask [name]` | Answers what is unanswered, or one named variable; `--all` asks everything again |
|
|
968
|
+
| `sous vars ask --file <path>` | Asks the definitions in a standalone file holding the same `variables:` array a recipe manifest carries |
|
|
969
|
+
|
|
970
|
+
`sous vars list`, `sous vars show` and `sous vars ask` all accept `--file`, and `sous vars ask`
|
|
971
|
+
accepts `--dry-run`.
|
|
972
|
+
|
|
973
|
+
Answers already in scope are listed visibly and never re-asked. Without a terminal, an
|
|
974
|
+
unanswered required variable fails the run and names the exact environment variables that would
|
|
975
|
+
satisfy it, most specific first, which is what a continuous integration log needs.
|
|
976
|
+
|
|
977
|
+
## Releasing and contributing
|
|
978
|
+
|
|
979
|
+
Two commands work inside a recipe repository rather than inside a project, so neither one looks
|
|
980
|
+
for a `.sous/` directory: `sous repo release` publishes, and `sous repo submit` proposes a change
|
|
981
|
+
to a repository someone else maintains. Both refuse to do anything until the repository describes
|
|
982
|
+
itself consistently.
|
|
983
|
+
|
|
984
|
+
### What is checked
|
|
985
|
+
|
|
986
|
+
Every run of either command checks the same things, and reports all of them at once rather than
|
|
987
|
+
stopping at the first:
|
|
988
|
+
|
|
989
|
+
- Every folder listed under `recipes` in `sous.repo.yaml` exists and holds exactly one recipe
|
|
990
|
+
manifest.
|
|
991
|
+
- Every recipe belongs to a namespace the repository manifest declares, and no two recipes share
|
|
992
|
+
a `namespace/name` key.
|
|
993
|
+
- Every `depends` and `subscribes` ref parses.
|
|
994
|
+
- No two variable definitions of DIFFERENT names claim the same environment variable. Two
|
|
995
|
+
definitions of the SAME name may share one, because that is exactly what the shared rung of the
|
|
996
|
+
resolution ladder is for.
|
|
997
|
+
- A tag exists for the version its recipe manifest declares, or that version is pending; when the
|
|
998
|
+
tag does exist, the manifest carried by that tag declares the same version, and the folder's
|
|
999
|
+
content still matches what the tag published.
|
|
1000
|
+
|
|
1001
|
+
Claiming a well-known name such as `PATH`, `HOME`, `GITHUB_TOKEN` or anything beginning `AWS_` or
|
|
1002
|
+
`SOUS_` is a warning rather than an error. Binding an existing token is legitimate; it just
|
|
1003
|
+
deserves saying out loud. Add `x-intentional: true` to the definition to say you meant it.
|
|
1004
|
+
|
|
1005
|
+
### Versions, tags and the index
|
|
1006
|
+
|
|
1007
|
+
Recipe metadata is the source of truth for versions. A tag shaped `namespace/recipe@1.2.3` is a
|
|
1008
|
+
convenience ref that records which commit a version was published from, and `sous.index.json` is
|
|
1009
|
+
the catalog subscribers read. The three are kept in step by three rules:
|
|
1010
|
+
|
|
1011
|
+
- **A published version never changes.** Its hash is carried forward exactly as published, and a
|
|
1012
|
+
disagreement is an error telling you to bump the version rather than republish it.
|
|
1013
|
+
- **A version is published when its tag exists.** The one moment an index records a version
|
|
1014
|
+
without a tag is the release commit itself: the index is committed and the tag is cut on that
|
|
1015
|
+
commit. Any older version missing its tag is an error.
|
|
1016
|
+
- **The tags are the backstop.** A tagged version missing from the index is rebuilt from its tag,
|
|
1017
|
+
so deleting `sous.index.json` and regenerating it restores the same catalog.
|
|
1018
|
+
|
|
1019
|
+
!> A release commits the version bumps and the index it writes, and nothing else. It refuses to
|
|
1020
|
+
run while anything else is uncommitted, because a tag names one commit and the index records
|
|
1021
|
+
what each recipe folder holds right now.
|
|
1022
|
+
|
|
1023
|
+
### `sous repo release`
|
|
1024
|
+
|
|
1025
|
+
One run plans, asks once, and then publishes. Within its scope it releases only recipes whose
|
|
1026
|
+
files changed since the tag that last published them, bumping any whose version still equals
|
|
1027
|
+
that tag.
|
|
1028
|
+
|
|
1029
|
+
| Invocation | What it does |
|
|
1030
|
+
|------------|--------------|
|
|
1031
|
+
| `sous repo release` | Plan, ask once, then bump, regenerate the index, commit and tag. |
|
|
1032
|
+
| `sous repo release --dry-run` | Print the plan and stop. |
|
|
1033
|
+
| `sous repo release --yes` | Skip the question; everything else is the same. |
|
|
1034
|
+
| `sous repo release --namespace <ns>` | Release only that namespace. Repeatable. |
|
|
1035
|
+
| `sous repo release --recipe <ns/name>` | Release only that recipe. Repeatable. |
|
|
1036
|
+
| `sous repo release --bump <level>` | `patch` (the default), `minor`, `major` or `prerelease`. |
|
|
1037
|
+
| `sous repo release --no-bump` | Raise nothing; a changed recipe nobody raised is an error. |
|
|
1038
|
+
| `sous repo release --include-unchanged` | Release everything in scope, changed or not. |
|
|
1039
|
+
| `sous repo release --tag` | Cut the tags even on a branch other than the default one. |
|
|
1040
|
+
| `sous repo release --push` | Push the commit, and the tags this run created, to `origin`. |
|
|
1041
|
+
| `sous repo release --check` | Read only: validate, and fail when the committed index is out of date. This is what a pull request runs. |
|
|
1042
|
+
| `sous repo release --ci` | The merge preset: never bump, never ask, fail on anything unbumped. |
|
|
1043
|
+
|
|
1044
|
+
Tags are cut dependency-first, and each version's resolved dependencies are written into the
|
|
1045
|
+
index. On a branch other than the default one a release bumps and commits but cuts no tags,
|
|
1046
|
+
because tags are cut on the default branch by the merge; `--tag` overrides that.
|
|
1047
|
+
|
|
1048
|
+
A version bump edits the manifest in place, so its comments, its field order and its layout
|
|
1049
|
+
survive. Two small normalizations happen in a YAML manifest: a folded block of prose may be
|
|
1050
|
+
re-wrapped, and the spacing before a trailing comment is collapsed to one space.
|
|
1051
|
+
|
|
1052
|
+
The workflow `sous repo init` scaffolds runs `--check` on every pull request and `--ci --push`
|
|
1053
|
+
on a merge.
|
|
1054
|
+
|
|
1055
|
+
### `sous repo submit`
|
|
1056
|
+
|
|
1057
|
+
`submit` means "propose a change for maintainers to review". It never publishes and never writes
|
|
1058
|
+
to a repository directly.
|
|
1059
|
+
|
|
1060
|
+
Sous validates first, because a proposal that fails the maintainer's own checks wastes their
|
|
1061
|
+
review, and then hands the mechanics to the provider's own command line tool, which already holds
|
|
1062
|
+
your credentials:
|
|
1063
|
+
|
|
1064
|
+
1. **Preflight.** An `origin` remote exists, sous recognizes its provider, that provider's CLI
|
|
1065
|
+
(`gh` or `glab`) is installed and signed in, and everything is committed.
|
|
1066
|
+
2. **Validation.** The repository validates, and the committed index is current.
|
|
1067
|
+
3. **Delegation.** On GitHub, sous asks whether you can push to the repository itself; if you
|
|
1068
|
+
cannot, it forks it onto your own account and proposes from there. The branch is pushed and a
|
|
1069
|
+
pull request (a merge request on GitLab) is opened. A change sitting on the default branch is
|
|
1070
|
+
moved to a branch named `sous/submit-<date>-<time>` first.
|
|
1071
|
+
|
|
1072
|
+
| Flag | What it does |
|
|
1073
|
+
|------|--------------|
|
|
1074
|
+
| `--title <text>` | The proposal's title. Defaults to your last commit's subject. |
|
|
1075
|
+
| `--body <text>` | The proposal's body. Defaults to a summary sous writes, listing the recipes and the versions the change would publish. |
|
|
1076
|
+
| `--draft` | Opens the proposal as a draft. |
|
|
1077
|
+
| `--dry-run` | Runs the whole preflight and sends nothing. |
|
|
1078
|
+
|
|
1079
|
+
Every step prints before it runs, and a failure says exactly which steps completed. A pushed
|
|
1080
|
+
branch with no proposal behind it is a normal outcome of a network failure, and you are told
|
|
1081
|
+
about it rather than left guessing.
|
|
1082
|
+
|
|
1083
|
+
?> If a repository's provider cannot open a proposal for you, sous prints the `contribute` pointer
|
|
1084
|
+
from its `sous.repo.yaml` instead, so you are never left without a route.
|