@sous-io/sous 0.2.32 → 0.2.33

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 (103) hide show
  1. package/README.md +7 -6
  2. package/docs/markdown/_sidebar.md +1 -0
  3. package/docs/markdown/commands.md +8 -4
  4. package/docs/markdown/configuration.md +28 -4
  5. package/docs/markdown/repositories-authoring.md +22 -2
  6. package/docs/markdown/repositories-consuming.md +40 -11
  7. package/docs/markdown/repositories-file-formats.md +28 -13
  8. package/docs/markdown/repositories-quickstart.md +1 -1
  9. package/docs/markdown/repositories.md +8 -4
  10. package/package.json +2 -1
  11. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +25 -17
  12. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  13. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  14. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  15. package/sous.config.schema.json +27 -15
  16. package/src/commands/lock/rebuild.ts +3 -6
  17. package/src/commands/namespace/show.ts +2 -2
  18. package/src/commands/recipe/show.ts +26 -9
  19. package/src/commands/repo/contribute.ts +32 -23
  20. package/src/commands/repo/gc.ts +1 -1
  21. package/src/commands/repo/remove.ts +1 -1
  22. package/src/commands/repo/search.ts +1 -1
  23. package/src/commands/vars/ask.ts +75 -54
  24. package/src/lib/build-service.ts +85 -11
  25. package/src/lib/config-schema.ts +59 -19
  26. package/src/lib/include-resolver.ts +307 -69
  27. package/src/lib/markdown-compiler.ts +273 -81
  28. package/src/lib/project-scaffold/templates.ts +19 -11
  29. package/src/lib/repos/catalog-inputs.ts +1 -1
  30. package/src/lib/repos/catalog.ts +98 -72
  31. package/src/lib/repos/declarations.ts +22 -12
  32. package/src/lib/repos/formats/index-file.ts +1 -1
  33. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  34. package/src/lib/repos/lock-service.ts +1 -1
  35. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  36. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  37. package/src/lib/repos/locked-recipes.ts +1 -1
  38. package/src/lib/repos/memories-hash-name.ts +33 -0
  39. package/src/lib/repos/namespace-resolver.ts +198 -119
  40. package/src/lib/repos/recipe-config-layers.ts +29 -1
  41. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  42. package/src/lib/repos/recipe-memories.ts +175 -0
  43. package/src/lib/repos/recipe-targets.ts +27 -28
  44. package/src/lib/repos/ref-search.ts +1 -1
  45. package/src/lib/repos/release/index-builder.ts +23 -15
  46. package/src/lib/repos/release/plan.ts +14 -7
  47. package/src/lib/repos/release/settle.ts +38 -121
  48. package/src/lib/repos/release/submit-checkout.ts +42 -23
  49. package/src/lib/repos/release/tags.ts +1 -1
  50. package/src/lib/repos/resolver.ts +90 -72
  51. package/src/lib/repos/subscription-service.ts +192 -170
  52. package/src/lib/settings.ts +37 -33
  53. package/src/lib/vars/ask.ts +3 -3
  54. package/src/lib/vars/mappings.ts +17 -9
  55. package/src/services/ref-resolver/container.ts +107 -0
  56. package/src/services/ref-resolver/dependency.ts +60 -0
  57. package/src/services/ref-resolver/format.ts +197 -0
  58. package/src/services/ref-resolver/glob.ts +115 -0
  59. package/src/services/ref-resolver/hash-names.ts +155 -0
  60. package/src/services/ref-resolver/index.ts +55 -0
  61. package/src/services/ref-resolver/injectable.ts +36 -0
  62. package/src/services/ref-resolver/location.ts +62 -0
  63. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  64. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  65. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  66. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  67. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  68. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  69. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  70. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  71. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  72. package/src/services/ref-resolver/named-refs.ts +144 -0
  73. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  74. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  75. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  76. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  77. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  78. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  79. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  80. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  81. package/src/services/ref-resolver/parts.ts +115 -0
  82. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  83. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  84. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  85. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  86. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  87. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  88. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  89. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  90. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  91. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  92. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  93. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  94. package/src/services/ref-resolver/source.ts +48 -0
  95. package/src/services/ref-resolver/tokens.ts +25 -0
  96. package/src/services/ref-resolver/types.ts +155 -0
  97. package/src/templating/init-liquid-engine.ts +25 -18
  98. package/src/lib/refs/find.ts +0 -770
  99. package/src/lib/refs/index.ts +0 -14
  100. package/src/lib/refs/parse.ts +0 -844
  101. package/src/lib/refs/pick.ts +0 -147
  102. package/src/lib/refs/scopes.ts +0 -95
  103. package/src/lib/refs/settle.ts +0 -219
package/README.md CHANGED
@@ -70,8 +70,8 @@ sous init
70
70
 
71
71
  `sous init` writes the project's `.sous/` directory and runs the first build. It creates a
72
72
  commented `sous.config.js` (pass `--format json` for a JSON config bound to the shipped
73
- schema), a starter prompt at `.sous/memories/AGENTS.md` that the config compiles to `AGENTS.md`
74
- at the project root, the two env files described below, and a `.sous/.gitignore` covering the
73
+ schema), a starter prompt at `.sous/memories/AGENTS.tpl.md` that the config compiles to `AGENTS.md`
74
+ at the project root (it includes every memory your subscribed recipes publish, with one line, `@#memories/**/*.md`), the two env files described below, and a `.sous/.gitignore` covering the
75
75
  files sous keeps local to one machine. A project with a `package.json` also gains `@sous-io/sous`
76
76
  as a devDependency at the running version, so the project pins the sous it builds with; run
77
77
  your package manager's install afterwards. The first build compiles the starter prompt and the
@@ -87,12 +87,12 @@ export const config = {
87
87
  compilation: {
88
88
  targets: [
89
89
  {
90
- entryPoint: "${sousDir}/memories/AGENTS.md",
90
+ entryPoint: "${sousDir}/memories/AGENTS.tpl.md",
91
91
  outputs: [{ destinationFile: "${projectRoot}/AGENTS.md" }],
92
92
  },
93
93
  ],
94
94
  },
95
- recipeOutputs: { skills: ["${projectRoot}/.claude/skills"] },
95
+ recipes: { skills: { outputs: ["${projectRoot}/.claude/skills"] } },
96
96
  };
97
97
  ```
98
98
 
@@ -179,8 +179,9 @@ A recipe's files are reachable without knowing where anything is installed. An `
179
179
  path may name a recipe by its namespace, so
180
180
  `@~workflow/task-files/_partials/resume-task.md` composes a block published by the recipe
181
181
  `workflow/task-files`, at the version your project has pinned, into your own instruction
182
- file. `@~project/...` names your project's root, `@~/...` names your home directory, and you
183
- can define your own aliases with an `_aliases` block. Either spelling of a file finds it:
182
+ file. `@#project/...` names your project's root, `@~/...` names your home directory, and you
183
+ can define your own aliases with an `_aliases` block. A path may be a glob, and
184
+ `@#memories/**/*.md` includes the memories of every recipe your project subscribes to. Either spelling of a file finds it:
184
185
  `@shared.md` also finds `shared.tpl.md`, and the other way round.
185
186
 
186
187
  Sous records every file and directory it writes in a state file, `.sous/sous.state.json` by
@@ -31,3 +31,4 @@
31
31
  - [0010: The index describes each version's dependencies and questions](adrs/0010-index-describes-recipes.md)
32
32
  - [0011: `sous update` updates sous through the package managers](adrs/0011-self-update.md)
33
33
  - [0012: Recipes do not install Sous plugins](adrs/0012-recipes-and-plugins.md)
34
+ - [0013: One ref resolver, and recipe memories through a `#memories` view](adrs/0013-one-ref-resolver.md)
@@ -67,8 +67,8 @@ Every topic answers to both spellings of its name: `repo` and `repos`, `subscrip
67
67
  ### `sous init [DIRECTORY]`
68
68
  Sets a project up for sous: writes its `.sous/` directory, then runs the first build. It is the one command that
69
69
  runs before a config exists, and the starting point for a project that has never used sous. It writes a commented
70
- primary config, a starter prompt at `.sous/memories/AGENTS.md` that the config compiles to `AGENTS.md` at the
71
- project root, `.sous/.env` and `.sous/.env.local.example`, and the sous-managed block in `.sous/.gitignore`. A
70
+ primary config, a starter prompt at `.sous/memories/AGENTS.tpl.md` that the config compiles to `AGENTS.md` at the
71
+ project root (it holds the one line, `@#memories/**/*.md`, that includes every memory your subscribed recipes publish), `.sous/.env` and `.sous/.env.local.example`, and the sous-managed block in `.sous/.gitignore`. A
72
72
  project that has a `package.json` also gains `@sous-io/sous` in its `devDependencies`, at exactly the running
73
73
  version, unless it already depends on it; nothing is installed, so run your package manager's install afterwards.
74
74
  The first build compiles the starter prompt and the `core` skills, and pins them in `.sous/sous.lock.json`.
@@ -103,10 +103,14 @@ repositories that ask for newer versions. Every command that builds (`init`, `bu
103
103
  `repo remove`, `repo unlink`, `subscription add`, `subscription remove`, `subscription update`) runs this same
104
104
  build, so each one prints the same things.
105
105
 
106
- A compile error (a missing or circular include, a template that fails to render, a file sous cannot read) fails
107
- the build. Every target is still compiled and every error is listed, then the build exits `1`. An output whose
106
+ A compile error (a missing or circular include, a line that looks like an include but cannot be read as one, a
107
+ template that fails to render, a file sous cannot read) fails the build. Every target is still compiled and every error is listed, then the build exits `1`. An output whose
108
108
  target had an error is not written, so the copy from the last good build stays in place.
109
109
 
110
+ A build also warns about every memory an active recipe publishes that no output includes, naming the line to add
111
+ (`@#memories/**/*.md`) and the `recipes.memories.exclude` opt-out; see
112
+ [Recipe memories](repositories-consuming.md#recipe-memories).
113
+
110
114
  A build also lists each recipe this project uses that has a newer version within the range
111
115
  declared for it, beside the version pinned. It moves no pin; only always-pull moves one. The build reads upstream
112
116
  for this at most once per freshness window (`store.freshnessSeconds`, five minutes by default), gives a
@@ -47,10 +47,34 @@ are resolved at different stages and never mix.
47
47
 
48
48
  ## Templates and the `.tpl.` convention
49
49
 
50
- A file's name decides whether sous renders it. A file with `.tpl.` in its name is rendered through
51
- LiquidJS and lands with that segment stripped, so `SKILL.tpl.md` compiles to `SKILL.md`; a file
52
- without it is copied verbatim, however much it looks like a template. The convention applies to
53
- `entryPoint` and `entryGlob` targets alike, and to the files a recipe brings into your project.
50
+ A file's own name decides whether sous renders it, every time. A file with `.tpl.` in its name is rendered
51
+ through LiquidJS and lands with that segment stripped, so `SKILL.tpl.md` compiles to `SKILL.md`; a file
52
+ without it is never rendered, however much it looks like a template, even when a template includes it. A
53
+ `.tpl.` file included from a plain file still renders, by its own name, before its text is placed. The
54
+ convention applies to `entryPoint` and `entryGlob` targets alike, and to the files a recipe brings into your
55
+ project.
56
+
57
+ ## Include syntax
58
+
59
+ A line holding only `@path` includes that file's content. A path may be:
60
+
61
+ - relative to the including file (`@sections/context.md`);
62
+ - `${var}` substituted (`@${projectRoot}/prompts/x.md`);
63
+ - home-relative (`@~/notes/private.md`);
64
+ - an alias from `_aliases`, by its first segment (`@myAlias/doc.md`);
65
+ - a name sous or a plugin registers, written with the `#` sigil: `#project` is the project root
66
+ (`@#project/prompts/intro.md`);
67
+ - a recipe file, written with the `~` sigil and the recipe's namespace (`@~workflow/task-files/_partials/resume.md`);
68
+ - a view: `@#memories/**/*.md` includes every active recipe's memories (see
69
+ [Recipe memories](repositories-consuming.md#recipe-memories)).
70
+
71
+ A path may use glob syntax (`*`, `**`, `?`, `[..]`, `{a,b}`) and then includes every file it matches, in bytewise
72
+ path order. The same file may be included in more than one place; a circular include is an error, as is a line
73
+ that looks like an include (`@docs/notes.txt`, `@~workflow/missing`) but cannot be resolved, which fails the
74
+ build naming the file and the line. A line inside a fenced code block is left as written. An alias name may not
75
+ start with `~` or `#`. Reads of recipe files go through the
76
+ [ref resolver](adrs/0013-one-ref-resolver.md); [Including recipe files in your own templates](repositories.md#including-recipe-files-in-your-own-templates)
77
+ has the namespace rules.
54
78
 
55
79
  ## Composition
56
80
 
@@ -68,11 +68,31 @@ contents:
68
68
 
69
69
  `contents` groups are what a subscribing project receives, one group per kind, each with `include` glob patterns
70
70
  relative to the recipe folder (and optional `exclude` patterns). The four kinds are `skills`, `memories`,
71
- `prompts` and `config`; the kind decides where files land, which the subscriber maps with
72
- [`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land). Field tables are in the
71
+ `prompts` and `config`; the kind decides what happens to them in the subscriber's project, which the subscriber shapes with
72
+ [`recipes`](repositories-file-formats.md#recipes-what-each-content-kind-does). Field tables are in the
73
73
  [recipe manifest](repositories-file-formats.md#sousrecipeyaml-the-recipe-manifest) reference, and recipe
74
74
  metadata is the source of truth for the version: never edit `sous.index.json` by hand.
75
75
 
76
+ ### Shipping memories
77
+
78
+ A memory is a fragment of an agent's always-loaded instructions. A recipe ships one with a `contents` group of
79
+ `kind: memories`:
80
+
81
+ ```yaml
82
+ contents:
83
+ - kind: memories
84
+ include:
85
+ - memories/**/*.md
86
+ ```
87
+
88
+ Plain `.md` files are copied as they are; a file named `*.tpl.md` renders through LiquidJS with the subscriber's
89
+ answers in scope. The recipe never says where a memory lands. A subscriber includes every memory of every recipe
90
+ it holds directly with one line, `@#memories/**/*.md`, which lists a file at
91
+ `#memories/<namespace>/<recipe>/<path under the pattern's static base>`. A recipe held only through `depends` is
92
+ a library and contributes none. The build warns a subscriber whose output includes none of a memory it
93
+ publishes, so a memory is never silently lost. See
94
+ [Recipe memories](repositories-consuming.md#recipe-memories) for the subscriber's side.
95
+
76
96
  A recipe's files compile exactly the way a project's own `entryGlob` target does, so the
77
97
  [`.tpl.` convention](configuration.md#templates-and-the-tpl-convention) applies unchanged inside a recipe. A
78
98
  template may include a file from a recipe this one depends on through the reserved `~` sigil, resolved against
@@ -173,17 +173,46 @@ like `workflow/qa-helper` above, is fetched and pinned but stays out of your out
173
173
 
174
174
  ## Choose where the files land
175
175
 
176
- Recipe files compile the way one of your own `entryGlob` targets does, under the
177
- [`.tpl.` convention](configuration.md#templates-and-the-tpl-convention): a file with `.tpl.` in its
178
- name is rendered through LiquidJS and loses `.tpl.` on the way out, and anything else is copied
179
- verbatim. Where each content kind lands is your project's decision, under the
180
- [`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land) config key, which takes a list of
181
- directories for each of `skills`, `memories` and `prompts`; for example
182
- `recipeOutputs: { skills: ["${projectRoot}/.claude/skills", "${projectRoot}/.codex/skills"] }`.
183
- Only `skills` has a default, `<project root>/.claude/skills`, because that is where every agent
184
- looks; a kind with no destination is skipped and the build says so once, naming the key. Recipe
185
- outputs are tracked like every other file sous writes, so `sous prune` removes what an unsubscribed
186
- recipe used to write and `sous clear` removes all of it; neither reaches into a linked checkout.
176
+ Recipe files compile under the [`.tpl.` convention](configuration.md#templates-and-the-tpl-convention): a file
177
+ with `.tpl.` in its name is rendered through LiquidJS and loses `.tpl.` on the way out, and anything else is copied
178
+ verbatim. Skills are written to the directories named by the
179
+ [`recipes`](repositories-file-formats.md#recipes-what-each-content-kind-does) config key; for example
180
+ `recipes: { skills: { outputs: ["${projectRoot}/.claude/skills", "${projectRoot}/.codex/skills"] } }`. The
181
+ default is `<project root>/.claude/skills`, because that is where every agent looks. Written skills are tracked
182
+ like every other file sous writes, so `sous prune` removes what an unsubscribed recipe used to write and
183
+ `sous clear` removes all of it; neither reaches into a linked checkout.
184
+
185
+ ## Recipe memories
186
+
187
+ A memory is a recipe's fragment of an agent's instructions. Sous does not copy memories anywhere; your
188
+ instruction source includes them. Put one line in the file your `AGENTS.md` or `CLAUDE.md` is built from:
189
+
190
+ ```text
191
+ @#memories/**/*.md
192
+ ```
193
+
194
+ `#memories` is a view: it lists the memories of every recipe you hold directly (a subscription to it, to its
195
+ namespace, or a co-subscription of a recipe you hold) at the virtual path
196
+ `#memories/<namespace>/<recipe>/<path>`, and the line includes each one. A recipe held only through `depends`
197
+ adds none. `sous init` writes this line into its starter, `.sous/memories/AGENTS.tpl.md`.
198
+
199
+ Memories come in dependency order (a recipe after the recipes it depends on, ties by recipe key). Two config
200
+ lists under `recipes.memories` shape the rest, each entry a glob over `namespace/recipe` keys or a regular
201
+ expression written `/.../`:
202
+
203
+ ```js
204
+ recipes: {
205
+ memories: { first: ["communication/*"], exclude: ["tool-usage/automated-browser-tasks"] },
206
+ }
207
+ ```
208
+
209
+ - `first`: those recipes' memories lead.
210
+ - `exclude`: those recipes' memories are left out; this is how you opt a recipe out on purpose.
211
+
212
+ A glob narrows the view as it does any path: `@#memories/communication/**/*.md` includes only that namespace.
213
+ When a build finds a memory an active recipe publishes that no include of any output reached (by exact path, glob
214
+ or the view) and `exclude` does not name, it warns, naming the memory and the line to add. Subscribing alone
215
+ includes nothing, and the warning is how that is never silent.
187
216
 
188
217
  ## Answer the questions
189
218
 
@@ -80,9 +80,11 @@ contents: # what a subscriber actually receives
80
80
  | `contents`, `variables` | no | Contents default to an empty list, which is what a curated bundle publishes; a duplicated variable name, or two definitions claiming one `env`, is an error |
81
81
  | `submissions` | no | Whether this recipe takes proposed changes, winning over the repository's block; see [The submissions block](#the-submissions-block) |
82
82
 
83
- `contents[].kind` is `skills`, `memories`, `prompts` or `config`; there is no kind for a sous plugin. The
84
- first three are written to the destinations named by [`recipeOutputs`](#recipeoutputs-where-the-files-land);
85
- `config` entries become config layers instead. `include` needs at least one glob, and both lists are recipe-relative.
83
+ `contents[].kind` is `skills`, `memories`, `prompts` or `config`; there is no kind for a sous plugin. `skills` are
84
+ written to the destinations named by [`recipes`](#recipes-what-each-content-kind-does); `memories` are included
85
+ into your instruction files by one line (see [Recipe memories](repositories-consuming.md#recipe-memories));
86
+ `config` entries become config layers; nothing takes `prompts` yet, and the build warns about a recipe that
87
+ publishes them. `include` needs at least one glob, and both lists are recipe-relative.
86
88
 
87
89
  ### The submissions block
88
90
 
@@ -124,9 +126,10 @@ two readings that both publish what is named fail it too, naming the spellings t
124
126
 
125
127
  ### Ref forms
126
128
 
127
- Every place a ref is written reads it with one parser, told where the ref came from. These forms are
128
- recognized everywhere; the table after them says which each place allows. A refused form is an error saying
129
- what to write there instead.
129
+ Every place a ref is written reads it with one ref resolver (`src/services/ref-resolver/`): a parser that
130
+ returns every reading of the text, a pruner for the place that drops the readings the place refuses, and a
131
+ lookup that keeps the readings that exist. These forms are recognized everywhere; the table after them says
132
+ which each place allows. A refused form is an error saying what to write there instead.
130
133
 
131
134
  | Form | Example | Names |
132
135
  | --- | --- | --- |
@@ -153,6 +156,7 @@ end of the project path with `/-/`, so it reads one way.
153
156
  | A config file's subscription keys | `namespace` or `namespace/recipe`, lowercase; the repository is recorded in the lockfile and a range in the entry's `range` field |
154
157
  | A recipe manifest's `depends` and `subscribes` | every form naming a namespace or a recipe, except `repo:` and a local location |
155
158
  | A key sous stores (the lockfile, the index, the store) | `namespace` or `namespace/recipe`, lowercase |
159
+ | An include line, after its `~` | `namespace/recipe/path`, any part a glob, optionally starting with a `repo:` qualifier; see [Include syntax](configuration.md#include-syntax) |
156
160
 
157
161
  Matching tries the exact spelling first, then ignores case; a name that still matches several things is a
158
162
  question, answered by `--accept-first` or by choosing. What sous writes (config layers, the lockfile, the
@@ -368,8 +372,9 @@ store: # every value is a default you can change
368
372
  maxBytes: 1073741824 # one gigabyte; past it, 'sous repo gc' evicts unpinned entries
369
373
  freshnessSeconds: 300 # how long a fetched index stays fresh
370
374
  watchPollSeconds: 300 # how often watch mode polls upstream
371
- recipeOutputs: # 'memories' and 'prompts' take the same list-of-paths shape
372
- skills: ["${projectRoot}/.claude/skills", "${projectRoot}/.codex/skills"]
375
+ recipes: # what each content kind a subscribed recipe publishes does
376
+ skills: { outputs: ["${projectRoot}/.claude/skills", "${projectRoot}/.codex/skills"] }
377
+ memories: { first: ["communication/*"], exclude: ["tool-usage/automated-browser-tasks"] }
373
378
  varMappings:
374
379
  QA_SERVICE_TOKEN: workflow/qa-variables/qaServiceToken
375
380
  ```
@@ -380,7 +385,7 @@ varMappings:
380
385
  | `subscriptions` | a ref key | A bare namespace (every recipe in it, including ones published later) or `namespace/recipe`. No repo qualifier and no range in the key |
381
386
  | `enabled: false` | either of the two above | The opt-out for the entries sous provides itself, the `sous-recipes` repository and the `core` subscription |
382
387
  | `store` | fixed fields | Plain numbers, all optional; the values sous ships are defaults, not assumptions |
383
- | `recipeOutputs` | content kind | Destination directories per kind; see [recipeOutputs: where the files land](#recipeoutputs-where-the-files-land) |
388
+ | `recipes` | content kind | What each kind does; see [recipes: what each content kind does](#recipes-what-each-content-kind-does) |
384
389
  | `varMappings` | environment variable name | Binds one name to one recipe variable, written `namespace/recipe/variableName` with an optional `repo:` qualifier |
385
390
 
386
391
  A repository's `url` may be a URL or an absolute path to one on this machine, read by the `local` provider
@@ -388,11 +393,21 @@ with identical trust. A hand-written entry is laid over sous's default field by
388
393
  `{ enabled: false }` is a complete opt-out, and a written `url` repoints the repository while the rest of
389
394
  the default stands.
390
395
 
391
- ### recipeOutputs: where the files land
396
+ ### recipes: what each content kind does
392
397
 
393
- Where each content kind lands, `${var}` substituted as in any config path. Only `skills` has a default,
394
- `<project root>/.claude/skills`; sous cannot guess where memories or prompts go, so a kind with no
395
- destination is skipped and one warning names this key. A recipe's `config` contents become layers, not files.
398
+ One block per content kind a subscribed recipe publishes. A recipe's `config` contents become layers, not files,
399
+ and a recipe's own config layer may set `recipes.skills` only; `recipes.memories` chooses what the project
400
+ includes, so it is dropped from a recipe's layer with a warning.
401
+
402
+ - `recipes.skills.outputs` (`string[]`, default `["<project root>/.claude/skills"]`): the directories recipe skill
403
+ bundles are written into, `${var}` substituted as in any config path. Name several to feed more than one agent.
404
+ - `recipes.memories.first` (`string[]`, default none): recipes whose memories lead the `#memories` view. An entry
405
+ is a glob over `namespace/recipe` keys (`communication/*`), or a regular expression written between two slashes
406
+ (`/^tool-usage\//`); a string, because a config layer is JSON and cannot hold a `RegExp`.
407
+ - `recipes.memories.exclude` (`string[]`, default none): recipes whose memories the view leaves out, and the
408
+ build's unincluded-memory warning never lists. The same entry forms as `first`.
409
+
410
+ There is no `prompts` block.
396
411
 
397
412
  ## Managed config layers
398
413
 
@@ -145,7 +145,7 @@ its name is rendered through LiquidJS and loses that segment (`SKILL.tpl.md` bec
145
145
  and any other file is copied verbatim. They are tracked like every file sous writes, so
146
146
  `sous prune` removes them when you unsubscribe. Where each kind lands is your project's decision,
147
147
  under
148
- [`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land).
148
+ [`recipes`](repositories-file-formats.md#recipes-what-each-content-kind-does).
149
149
 
150
150
  ## 6. Start a repository of your own
151
151
 
@@ -280,8 +280,10 @@ $ sous build
280
280
  ✓ /home/me/my-project/.claude/skills/start-task/SKILL.md (~1,996 tokens)
281
281
  ```
282
282
 
283
- Skills default to `<project root>/.claude/skills`; every other content kind needs a destination in
284
- the [`recipeOutputs`](repositories-file-formats.md#recipeoutputs-where-the-files-land) block.
283
+ Skills default to `<project root>/.claude/skills`, and the
284
+ [`recipes`](repositories-file-formats.md#recipes-what-each-content-kind-does) block changes that. Memories are
285
+ not copied anywhere; one include line in your instruction source pulls them in (see
286
+ [Recipe memories](repositories-consuming.md#recipe-memories)).
285
287
 
286
288
  By default a build uses what the lockfile pins and does not talk to the network. Two things
287
289
  change that: **always-pull**, which installs a newer in-range version whenever one exists, and
@@ -325,13 +327,15 @@ README. None is ever overwritten, and directories holding rendered output are le
325
327
  ## Including recipe files in your own templates
326
328
 
327
329
  A template may pull in a file from a recipe through the reserved `~` sigil, naming the recipe's
328
- published identity and then the path inside it, on a line of its own:
330
+ published identity and then the path inside it, on a line of its own. Any part may be a glob, and a
331
+ `repo:` qualifier may start the path to say which repository when two publish the same namespace:
329
332
 
330
333
  ```text
331
334
  @~workflow/qa-helper/_partials/review-steps.md
335
+ @~workflow/qa-helper/_partials/*.md
332
336
  ```
333
337
 
334
- The `~` is required. A bare `@path` is always a relative path or a declared alias, so an include
338
+ A glob includes every file it matches, in bytewise path order. The `~` is required. A bare `@path` is always a relative path or a declared alias, so an include
335
339
  line can never quietly stop meaning a file on disk. In your own templates, `~<namespace>`
336
340
  resolves against every recipe the lockfile pins, whatever brought it in: a recipe you subscribe
337
341
  to, a member of a set you subscribe to, or a library another recipe depends on. Inside a recipe,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sous-io/sous",
3
- "version": "0.2.32",
3
+ "version": "0.2.33",
4
4
  "description": "Compiles AI coding agent configuration (CLAUDE.md, skills, memories) from LiquidJS templates",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -57,6 +57,7 @@
57
57
  "@oclif/core": "^4",
58
58
  "chokidar": "^5.0.0",
59
59
  "glob": "^13.0.6",
60
+ "inversify": "^8.2.3",
60
61
  "jsonc-parser": "^3.3.1",
61
62
  "liquidjs": "^10.25.0",
62
63
  "minimatch": "^10.2.4",
@@ -92,7 +92,7 @@ Include another file at render time (path **relative to the template file's dire
92
92
  {% endraw %}
93
93
 
94
94
  `render` resolves paths relative to the template file. For files outside that tree, use
95
- a recipe reference (`@~<namespace>/<recipe>/...`), a path **alias** (`@~project/...`, or a
95
+ a recipe reference (`@~<namespace>/<recipe>/...`), a path **alias** (`@#project/...`, or a
96
96
  user-defined alias), a home-relative path (`@~/...`) or a `@`-prefixed `${var}` path; the
97
97
  same resolution as `@include` (see below) works in `render` too.
98
98
 
@@ -107,23 +107,24 @@ The `@path` syntax is processed by the build system before LiquidJS runs. It inc
107
107
  a file's content inline. Write `@` immediately followed by a `.md` path on its own line
108
108
  with nothing else on that line.
109
109
 
110
- `@include` works in both `.tpl.` and plain `.md` files. Included content is subject to
111
- LiquidJS rendering if the parent file is a `.tpl.` (so a Liquid tag inside the included
112
- file runs in the parent's render pass).
110
+ `@include` works in both `.tpl.` and plain `.md` files. A file's own name decides whether it
111
+ renders, every time: a `.tpl.` file always renders as Liquid and any other file never does, wherever
112
+ it is included from. Each included file renders on its own, by its own name, with the including
113
+ output's variables, and its text is then placed where the line was. A plain `.md` file that holds
114
+ {% raw %}`{{ tags }}`{% endraw %} therefore keeps them verbatim; rename it `.tpl.md` when it needs rendering.
113
115
 
114
116
  The engine sets both `strictVariables: false` and `strictFilters: false`. An undefined
115
117
  variable renders as an empty string, and an **unknown filter silently no-ops**, passing
116
118
  its input through unchanged. Neither mistake raises an error, so a typo in a variable or
117
119
  filter name shows up only as missing or unfiltered output.
118
120
 
119
- ### Gotcha: `@include` fires inside fenced code blocks
121
+ ### Fenced code blocks, repeats and malformed lines
120
122
 
121
- The `@include` processor runs on the raw file content *before* LiquidJS and has no
122
- markdown awareness whatsoever; it matches any line that is nothing but an `@`-prefixed
123
- `.md` path. A fenced code block does not protect it: an `@path.md` line inside triple
124
- backticks is still executed and replaced with the file's content. There is no escape
125
- syntax. To show an `@`-path as an example, put something else on the line (indent it,
126
- prefix it with a word, or wrap it in backticks inline).
123
+ A line inside a fenced code block (``` or ~~~) is never an include, so an `@path.md` example
124
+ inside a fence shows as written. The same file may be included in more than one place; a circular
125
+ include is an error. A line that looks like an include (one word starting with `@`, holding a `/` or
126
+ `.md`, or starting with `~`, `#`, `.` or `$`) but cannot be resolved fails the build, naming the file
127
+ and the line; a line like `@alice thanks` is plain text.
127
128
 
128
129
  ### Path forms
129
130
 
@@ -133,12 +134,14 @@ A `@`-path may be any of:
133
134
  - **Variable-substituted**: `@${projectRoot}/prompts/x.md`; `${var}` is
134
135
  substituted before resolving; if the result is absolute it is used directly.
135
136
  - **Aliased**: `@<alias>/rest.md`, where the first segment names a registered alias.
137
+ - **A glob**: `*`, `**`, `?`, `[..]` and `{a,b}` after substitution, as in `entryGlob`; it
138
+ includes every file it matches, in bytewise path order.
136
139
 
137
140
  ### Recipe references and path aliases
138
141
 
139
142
  The first path segment, up to the first `/` or `:` (both separators work, so `@a/b.md`
140
- is the same as `@a:b.md`), is matched first against the alias registry and then, when it begins with
141
- `~`, against the recipe namespaces this project or recipe can address.
143
+ is the same as `@a:b.md`), is matched first against the alias registry (which holds the `#` names) and then, when it begins
144
+ with `~`, against the recipe namespaces this project or recipe can address.
142
145
 
143
146
  The normal way to reach a file that another recipe publishes is a **recipe reference**,
144
147
  written `@~<namespace>/<recipe>/<path inside that recipe>`:
@@ -152,10 +155,15 @@ may address every recipe the project's lockfile pins, whatever brought it in. An
152
155
  is an error naming what was missing, so a reference can never quietly pick up a recipe
153
156
  nobody asked for.
154
157
 
155
- Built-in **aliases** are reserved, always begin with `~`, and are consulted before recipe
156
- namespaces. There is exactly one:
158
+ Built-in names are reserved, begin with `#` (names sous or a plugin registers), and are consulted
159
+ before recipe namespaces:
157
160
 
158
- - `@~project/...` → the consuming project's root.
161
+ - `@#project/...` → the consuming project's root.
162
+ - `@#memories/**/*.md` → a **view**: every memory published by a recipe this project holds
163
+ directly (a subscription, or a co-subscription of one), at the virtual path
164
+ `#memories/<namespace>/<recipe>/<path>`, in dependency order. A recipe held only through
165
+ `depends` contributes none. The `recipes.memories.first` and `recipes.memories.exclude` config
166
+ lists reorder and filter it.
159
167
 
160
168
  A bare `@~/...` is not an alias: the sigil followed only by a separator is the home
161
169
  directory, so `@~/notes/context.md` includes a file from your home directory.
@@ -168,7 +176,7 @@ Everything else sous once shipped inside its own package is published as a recip
168
176
  a recipe reference is what reaches it.
169
177
 
170
178
  Projects register their own aliases in settings via an `_aliases` block (root and/or
171
- project level); names may **not** start with `~` (reserved). An alias value is a string
179
+ project level); names may **not** start with `~` or `#` (reserved). An alias value is a string
172
180
  or an array of strings (each may use `${var}`):
173
181
 
174
182
  ```
@@ -59,6 +59,10 @@ prefer the on-disk copies.
59
59
  - Hand-written config belongs in the primary file or your own `conf.d/` layers. Never
60
60
  hand-edit `conf.d/500-*` through `conf.d/599-*`; that band is reserved for layers the sous
61
61
  CLI writes.
62
+ - The top-level `recipes` key says what the project does with each content kind its subscribed
63
+ recipes publish: `recipes.skills.outputs` lists the directories skills are written to, and
64
+ `recipes.memories.first` and `recipes.memories.exclude` (globs over `namespace/recipe`, or
65
+ `/regex/` strings) order and filter the memories the `@#memories/**/*.md` include pulls in.
62
66
  - Any config problem halts sous with a ConfigError naming the offending file; fix the named
63
67
  file rather than working around it.
64
68
  - Configs use `${var}`; template files use LiquidJS double-brace syntax. The two resolve at
@@ -49,10 +49,12 @@ The fields in that example:
49
49
  `sous repo search`.
50
50
  - `contents` is what a subscriber receives: one group per kind (`skills`, `memories`, `prompts` or
51
51
  `config`), each with `include` globs and optional `exclude` globs, relative to the recipe folder.
52
- The subscribing project decides where skills, memories and prompts land. A `config` file becomes
52
+ The subscribing project decides where skills land; memories are included into its instruction
53
+ files by one line, `@#memories/**/*.md` (see "Shipping Memories" below), and nothing takes
54
+ prompts yet. A `config` file becomes
53
55
  a config layer in the project, loaded under the project's own config so the project always wins;
54
56
  it MUST be `.json`, `.jsonc`, `.yaml` or `.yml`, and every key outside `_aliases`, `_vars`,
55
- `compilation`, `recipeOutputs`, `runtimeContext`, `store` and `varMappings` is dropped with a
57
+ `compilation`, `recipes`, `runtimeContext`, `store` and `varMappings` is dropped with a
56
58
  warning. A recipe that only bundles others may leave `contents` out.
57
59
  - A recipe MAY install tool plugins (a plugin for a coding agent tool, such as a Claude Code
58
60
  plugin) and MUST NOT install a Sous plugin (one that extends sous itself). Sous plugins come only
@@ -161,13 +163,24 @@ Each entry names its target by where it lives:
161
163
  `local://` locators and `repo:` short names are refused: both are one project's private view of a
162
164
  repository, and a published manifest is read everywhere.
163
165
 
166
+ ## Shipping Memories
167
+
168
+ A memory is a fragment of an agent's always-loaded instructions. Publish one with a `contents`
169
+ group of `kind: memories`, for example `include: [memories/**/*.md]`. A plain `.md` file is copied
170
+ as it is; name a file `*.tpl.md` when it needs the subscriber's answers. The recipe never says where
171
+ a memory lands: a subscribing project includes the memories of every recipe it holds directly with
172
+ one line, `@#memories/**/*.md`, at the virtual path `#memories/<namespace>/<recipe>/<path>` (the path
173
+ being relative to the pattern's static base). A recipe held only through `depends` is a library and
174
+ contributes none, and the subscriber's build warns about a published memory no output includes.
175
+
164
176
  ## Includes and Partials
165
177
 
166
178
  A line holding only `@~<namespace>/<recipe>/<path>.md`, starting at the first column, includes that
167
179
  file from a recipe at its pinned version. Inside a recipe it resolves only against the recipe
168
180
  itself and the recipes it lists under `depends` or `subscribes`; the path may not be absolute or
169
- contain `.` or `..` segments, and it MUST end in `.md` (`notes.md` also finds `notes.tpl.md`). A
170
- line inside a fenced code block is never an include.
181
+ contain `.` or `..` segments, and it MUST end in `.md` (`notes.md` also finds `notes.tpl.md`). Any
182
+ part may be a glob, which includes every match. A line inside a fenced code block is never an
183
+ include, and a line that looks like an include but cannot be read as one fails the build.
171
184
 
172
185
  A **partial** is a short piece of text several skills share, kept in the recipe's `_partials/`
173
186
  folder beside `skills/`. As long as no `contents` glob covers `_partials/`, it is never installed on
@@ -179,12 +192,11 @@ What to review: $ARGUMENTS
179
192
  @~review/checklist/_partials/review-rules.md
180
193
  ```
181
194
 
182
- The included text is pasted in before rendering, so any Liquid inside a partial renders only when
183
- the including file is a `.tpl.` file, and then with that file's variables. Every compiled source in
184
- the official sous repositories is named `*.tpl.md`, partials included, so a partial MAY use template
185
- variables: they render in every file that includes it. For example, a partial holding
195
+ A file's own name decides whether it renders: a `.tpl.` file always renders as Liquid, and any other
196
+ file never does, wherever it is included from. So a partial that uses template variables MUST be named
197
+ `*.tpl.md`, and it renders with the including output's variables in every file that includes it. For
198
+ example, a partial holding
186
199
  `{% raw %}Task files live in {{ taskFileRoot }}.{% endraw %}` renders with the project's answer wherever it is included.
187
- Name a new partial `*.tpl.md` too.
188
200
 
189
201
  ## Source for this Skill
190
202
 
@@ -11,7 +11,7 @@ formatVersion: 1
11
11
 
12
12
  namespace: core
13
13
  name: sous-skills
14
- version: 0.2.32
14
+ version: 0.2.33
15
15
 
16
16
  description: >-
17
17
  The skills that teach an agent what sous is and how it works: which files sous
@@ -295,26 +295,38 @@
295
295
  },
296
296
  "additionalProperties": false
297
297
  },
298
- "recipeOutputs": {
298
+ "recipes": {
299
299
  "type": "object",
300
300
  "properties": {
301
301
  "skills": {
302
- "type": "array",
303
- "items": {
304
- "type": "string"
305
- }
302
+ "type": "object",
303
+ "properties": {
304
+ "outputs": {
305
+ "type": "array",
306
+ "items": {
307
+ "type": "string"
308
+ }
309
+ }
310
+ },
311
+ "additionalProperties": false
306
312
  },
307
313
  "memories": {
308
- "type": "array",
309
- "items": {
310
- "type": "string"
311
- }
312
- },
313
- "prompts": {
314
- "type": "array",
315
- "items": {
316
- "type": "string"
317
- }
314
+ "type": "object",
315
+ "properties": {
316
+ "first": {
317
+ "type": "array",
318
+ "items": {
319
+ "type": "string"
320
+ }
321
+ },
322
+ "exclude": {
323
+ "type": "array",
324
+ "items": {
325
+ "type": "string"
326
+ }
327
+ }
328
+ },
329
+ "additionalProperties": false
318
330
  }
319
331
  },
320
332
  "additionalProperties": false
@@ -24,7 +24,7 @@ import { recipeFilesDirectory } from "../../lib/repos/catalog-inputs.js";
24
24
  import { readRecipeManifestIn } from "../../lib/repos/locked-recipes.js";
25
25
  import { createEmptyLockfile } from "../../lib/repos/formats/lockfile.js";
26
26
  import type { LockRepoInput } from "../../lib/repos/lock-service.js";
27
- import { RefSource, parseShortRef } from "../../lib/refs/index.js";
27
+ import { RefSource, parseNamedRef } from "../../services/ref-resolver/index.js";
28
28
  import {
29
29
  PROJECT_REQUESTER,
30
30
  resolveRefs,
@@ -133,12 +133,9 @@ export default class LockRebuild extends BaseCommand {
133
133
  .sort()
134
134
  .map((key) => {
135
135
  const entry = subscriptions[key]!;
136
- const parsed = parseShortRef(key, RefSource.Config);
137
136
  return {
138
- ref: {
139
- ...parsed,
140
- ...(entry.range === undefined ? {} : { range: entry.range }),
141
- },
137
+ ref: parseNamedRef(key, RefSource.Config),
138
+ ...(entry.range === undefined ? {} : { range: entry.range }),
142
139
  requestedBy: PROJECT_REQUESTER,
143
140
  kind: "subscribes" as const,
144
141
  ...(entry.prerelease === true ? { prerelease: true } : {}),