@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.
Files changed (205) hide show
  1. package/README.md +115 -35
  2. package/bin/run.js +10 -1
  3. package/docs/markdown/README.md +27 -0
  4. package/docs/markdown/_sidebar.md +18 -0
  5. package/docs/markdown/commands.md +308 -0
  6. package/docs/markdown/config-discovery.md +74 -0
  7. package/docs/markdown/config-inspection.md +69 -0
  8. package/docs/markdown/config-layers.md +92 -0
  9. package/docs/markdown/config-variables.md +79 -0
  10. package/docs/markdown/configuration.md +71 -0
  11. package/docs/markdown/design-principles.md +59 -0
  12. package/docs/markdown/repositories-authoring.md +408 -0
  13. package/docs/markdown/repositories-consuming.md +580 -0
  14. package/docs/markdown/repositories-file-formats.md +1084 -0
  15. package/docs/markdown/repositories-variables.md +387 -0
  16. package/docs/markdown/repositories.md +303 -0
  17. package/docs/markdown/skill-categories.md +58 -0
  18. package/package.json +72 -8
  19. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/SKILL.tpl.md +20 -20
  20. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/about-something.md +2 -2
  21. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/examples/do-something.md +1 -1
  22. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/advanced-patterns.md +6 -6
  23. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/commands.md +5 -5
  24. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/frontmatter.md +3 -3
  25. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-liquid-templates/SKILL.tpl.md +40 -25
  26. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +70 -0
  27. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +75 -0
  28. package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/create-skill/SKILL.tpl.md +8 -9
  29. package/recipes/core/sous-skills/sous.recipe.yaml +45 -0
  30. package/sous.config.schema.json +337 -0
  31. package/src/base-command.ts +220 -67
  32. package/src/commands/build.ts +150 -73
  33. package/src/commands/clear.ts +23 -15
  34. package/src/commands/compile.ts +74 -16
  35. package/src/commands/config/get.ts +110 -0
  36. package/src/commands/config/show.ts +32 -0
  37. package/src/commands/config/validate.ts +53 -0
  38. package/src/commands/help.ts +46 -0
  39. package/src/commands/launch.ts +36 -14
  40. package/src/commands/lock/rebuild.ts +241 -0
  41. package/src/commands/lock/show.ts +115 -0
  42. package/src/commands/namespace/list.ts +117 -0
  43. package/src/commands/namespace/show.ts +110 -0
  44. package/src/commands/prune.ts +3 -11
  45. package/src/commands/recipe/list.ts +95 -0
  46. package/src/commands/recipe/show.ts +301 -0
  47. package/src/commands/repo/add.ts +145 -0
  48. package/src/commands/repo/gc.ts +172 -0
  49. package/src/commands/repo/init.ts +136 -0
  50. package/src/commands/repo/link.ts +500 -0
  51. package/src/commands/repo/list.ts +179 -0
  52. package/src/commands/repo/release.ts +619 -0
  53. package/src/commands/repo/remove.ts +193 -0
  54. package/src/commands/repo/search.ts +189 -0
  55. package/src/commands/repo/submit.ts +133 -0
  56. package/src/commands/repo/unlink.ts +147 -0
  57. package/src/commands/subscription/add.ts +285 -0
  58. package/src/commands/subscription/list.ts +129 -0
  59. package/src/commands/subscription/remove.ts +181 -0
  60. package/src/commands/vars/ask.ts +374 -0
  61. package/src/commands/vars/index.ts +79 -0
  62. package/src/commands/vars/list.ts +67 -0
  63. package/src/commands/vars/show.ts +77 -0
  64. package/src/config-command.ts +30 -0
  65. package/src/lib/build-service.ts +206 -54
  66. package/src/lib/config-discovery.ts +220 -27
  67. package/src/lib/config-inspect.ts +145 -0
  68. package/src/lib/config-kernel.mjs +377 -0
  69. package/src/lib/config-schema.ts +361 -0
  70. package/src/lib/env-file.ts +328 -0
  71. package/src/lib/env-local.ts +18 -1
  72. package/src/lib/errors.ts +32 -0
  73. package/src/lib/include-resolver.ts +108 -15
  74. package/src/lib/interactive.ts +165 -0
  75. package/src/lib/markdown-compiler.ts +118 -37
  76. package/src/lib/package-info.ts +25 -0
  77. package/src/lib/pid-service.ts +32 -21
  78. package/src/lib/refs/find.ts +589 -0
  79. package/src/lib/refs/index.ts +12 -0
  80. package/src/lib/refs/pick.ts +147 -0
  81. package/src/lib/refs/scopes.ts +61 -0
  82. package/src/lib/repos/catalog-display.ts +116 -0
  83. package/src/lib/repos/catalog-inputs.ts +160 -0
  84. package/src/lib/repos/catalog.ts +722 -0
  85. package/src/lib/repos/core-recipe.ts +105 -0
  86. package/src/lib/repos/defaults.ts +175 -0
  87. package/src/lib/repos/formats/common.ts +389 -0
  88. package/src/lib/repos/formats/index-file.ts +215 -0
  89. package/src/lib/repos/formats/links-map.ts +96 -0
  90. package/src/lib/repos/formats/lockfile.ts +167 -0
  91. package/src/lib/repos/formats/patterns.ts +57 -0
  92. package/src/lib/repos/formats/recipe-manifest.ts +395 -0
  93. package/src/lib/repos/formats/repo-manifest.ts +88 -0
  94. package/src/lib/repos/formats/store-entry.ts +84 -0
  95. package/src/lib/repos/freshness.ts +208 -0
  96. package/src/lib/repos/git-clone.ts +312 -0
  97. package/src/lib/repos/identity.ts +89 -0
  98. package/src/lib/repos/index.ts +58 -0
  99. package/src/lib/repos/links.ts +353 -0
  100. package/src/lib/repos/load-manifest.ts +236 -0
  101. package/src/lib/repos/lock-service.ts +453 -0
  102. package/src/lib/repos/locked-namespace-resolver.ts +90 -0
  103. package/src/lib/repos/locked-recipes.ts +254 -0
  104. package/src/lib/repos/managed-layer.ts +422 -0
  105. package/src/lib/repos/namespace-resolver.ts +370 -0
  106. package/src/lib/repos/providers/base.ts +206 -0
  107. package/src/lib/repos/providers/git.ts +233 -0
  108. package/src/lib/repos/providers/github.ts +294 -0
  109. package/src/lib/repos/providers/gitlab.ts +263 -0
  110. package/src/lib/repos/providers/http.ts +102 -0
  111. package/src/lib/repos/providers/index-cache.ts +382 -0
  112. package/src/lib/repos/providers/index.ts +106 -0
  113. package/src/lib/repos/providers/local.ts +391 -0
  114. package/src/lib/repos/providers/provider.ts +401 -0
  115. package/src/lib/repos/recipe-config-layers.ts +287 -0
  116. package/src/lib/repos/recipe-targets.ts +223 -0
  117. package/src/lib/repos/ref-search.ts +46 -0
  118. package/src/lib/repos/ref.ts +513 -0
  119. package/src/lib/repos/reference-report.ts +122 -0
  120. package/src/lib/repos/release/bump.ts +161 -0
  121. package/src/lib/repos/release/git-state.ts +305 -0
  122. package/src/lib/repos/release/index-builder.ts +635 -0
  123. package/src/lib/repos/release/index.ts +16 -0
  124. package/src/lib/repos/release/plan.ts +512 -0
  125. package/src/lib/repos/release/submit-service.ts +496 -0
  126. package/src/lib/repos/release/tags.ts +243 -0
  127. package/src/lib/repos/release/validate.ts +463 -0
  128. package/src/lib/repos/resolver.ts +789 -0
  129. package/src/lib/repos/scaffold/index.ts +238 -0
  130. package/src/lib/repos/scaffold/templates.ts +413 -0
  131. package/src/lib/repos/seed.ts +414 -0
  132. package/src/lib/repos/store/contract.ts +64 -0
  133. package/src/lib/repos/store/hash.ts +114 -0
  134. package/src/lib/repos/store/recipe-store.ts +599 -0
  135. package/src/lib/repos/store/settings.ts +58 -0
  136. package/src/lib/repos/subscription-service.ts +2678 -0
  137. package/src/lib/repos/trust.ts +447 -0
  138. package/src/lib/settings.ts +546 -189
  139. package/src/lib/sous-home.ts +104 -0
  140. package/src/lib/state.ts +52 -20
  141. package/src/lib/vars/ask.ts +1152 -0
  142. package/src/lib/vars/definition-source.ts +252 -0
  143. package/src/lib/vars/display.ts +233 -0
  144. package/src/lib/vars/index.ts +18 -0
  145. package/src/lib/vars/ladder.ts +282 -0
  146. package/src/lib/vars/mappings.ts +265 -0
  147. package/src/lib/vars/names.ts +94 -0
  148. package/src/lib/vars/preanswers.ts +395 -0
  149. package/src/lib/vars/question-plan.ts +218 -0
  150. package/src/lib/vars/report.ts +228 -0
  151. package/src/lib/vars/safe-regex.ts +235 -0
  152. package/src/lib/vars/validate.ts +312 -0
  153. package/src/lib/watch-loop.ts +148 -0
  154. package/src/templating/init-liquid-engine.ts +58 -16
  155. package/src/utils/choice-prompt.ts +143 -0
  156. package/src/utils/command-errors.ts +186 -0
  157. package/src/utils/command-help.ts +45 -0
  158. package/src/utils/confirm-prompt.ts +110 -0
  159. package/src/utils/flags.ts +153 -0
  160. package/src/utils/formatting.ts +540 -55
  161. package/src/utils/prompts.ts +35 -1
  162. package/src/utils/sous-directory.ts +245 -0
  163. package/src/utils/table.ts +603 -0
  164. package/src/utils/value-prompt.ts +119 -0
  165. package/shared-prompts/_partials/resume-task.md +0 -51
  166. package/shared-prompts/_partials/sub-agent-delegation.md +0 -32
  167. package/shared-prompts/_partials/update-task-file.md +0 -52
  168. package/shared-prompts/memories/automated-browser-tasks/INDEX.tpl.md +0 -52
  169. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/SKILL.tpl.md +0 -102
  170. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/auth-failure-handling.mjs +0 -81
  171. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/chained-workflow.mjs +0 -126
  172. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/examples/simple-fetch.mjs +0 -92
  173. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/architecture.md +0 -61
  174. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/auth-and-sessions.md +0 -65
  175. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/ctx-api.md +0 -96
  176. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/installation.md +0 -104
  177. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/references/script-conventions.md +0 -243
  178. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/chrome-state.mjs +0 -148
  179. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.mjs +0 -383
  180. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/debug.spec.mjs +0 -267
  181. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/eslint.config.mjs +0 -56
  182. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/harness.mjs +0 -169
  183. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/keyring.mjs +0 -59
  184. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/logger.mjs +0 -25
  185. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/params.mjs +0 -140
  186. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/run.mjs +0 -140
  187. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/settings.tpl.mjs +0 -1
  188. package/shared-prompts/skills/automated-browser-tasks/about-automated-browser-tasks/scripts/utils.mjs +0 -185
  189. package/shared-prompts/skills/automated-browser-tasks/create-automated-browser-task/SKILL.tpl.md +0 -52
  190. package/shared-prompts/skills/automated-browser-tasks/running-automated-browser-tasks/SKILL.tpl.md +0 -59
  191. package/shared-prompts/skills/automated-browser-tasks/update-automated-browser-task/SKILL.tpl.md +0 -47
  192. package/shared-prompts/skills/control-flow/approve/SKILL.tpl.md +0 -26
  193. package/shared-prompts/skills/control-flow/opine/SKILL.tpl.md +0 -58
  194. package/shared-prompts/skills/control-flow/repeat/SKILL.tpl.md +0 -27
  195. package/shared-prompts/skills/control-flow/research/SKILL.tpl.md +0 -34
  196. package/shared-prompts/skills/sous-skills/about-sous/SKILL.tpl.md +0 -51
  197. package/shared-prompts/skills/task-files/about-task-files/SKILL.tpl.md +0 -122
  198. package/shared-prompts/skills/task-files/continue-task-in-new-branch/SKILL.tpl.md +0 -80
  199. package/shared-prompts/skills/task-files/go/SKILL.tpl.md +0 -14
  200. package/shared-prompts/skills/task-files/resume-task/SKILL.tpl.md +0 -13
  201. package/shared-prompts/skills/task-files/start-task/SKILL.tpl.md +0 -93
  202. package/shared-prompts/skills/task-files/update/SKILL.tpl.md +0 -14
  203. package/shared-prompts/skills/task-files/update-task-file/SKILL.tpl.md +0 -13
  204. /package/{shared-prompts/skills/sous-skills → recipes/core/sous-skills/skills}/about-agent-skills/references/substitutions.md +0 -0
  205. /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.