yadflow 4.3.0 → 4.4.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.
@@ -0,0 +1,301 @@
1
+ // The recommendation catalogue — a short, hand-picked list of skills worth binding to each step (E52).
2
+ //
3
+ // yad skill recommend [<step>] [--profile <p> | --epic <id>] [--json]
4
+ //
5
+ // WHAT IT IS FOR. The toolbox lists three skill pools (BMAD-METHOD, ECC, mattpocock/skills) as
6
+ // "recommended — listed, never pushed". A pool holds dozens of skills, and an agent with three pools
7
+ // installed chooses between hundreds of descriptions. This list narrows that to a few named skills per
8
+ // step, each with the reason it fits. It recommends only: `yad skill bind` stays the one writer, and
9
+ // nothing is ever bound for you (E84: listed, never pushed; E51: a step's line is always a team's choice).
10
+ //
11
+ // A CHAIN, NEVER A REPLACEMENT. yadflow's own skill for a step does work no pool skill knows about — it
12
+ // writes the artifact where the gate looks, locks the contract, seeds the ledger. So every suggestion is
13
+ // the pool skill FIRST and yadflow's skill LAST: `yad skill bind architecture <pool-skill> yad-architecture`.
14
+ // A chain hands each skill what the one before it produced, and the last output is the artifact (closed
15
+ // decision 7), so the pool skill shapes the thinking and yadflow's skill files the result.
16
+ //
17
+ // VERSIONED SEPARATELY (decided with the row). The list carries its own `version` and `checked` day, apart
18
+ // from yadflow's package version, and every command that shows it prints its age. It still ships inside
19
+ // yadflow and moves with a release: no network, no cache, no project file, so no shape change. A test
20
+ // holds each version to one content: changing an entry without moving `version` fails.
21
+ //
22
+ // EVERY ENTRY IS VETTED, THE E88 WAY. Each one carries the same `vetted` record as a toolbox tool —
23
+ // where the licence was read, the official source, the release tag on that day — plus `file`, the exact
24
+ // SKILL.md at that tag, so the name was read from the real repository and never guessed from a pool's
25
+ // naming habit. The release check refuses to publish while any record is older than VET_MAX_AGE_DAYS
26
+ // (`scripts/vet-check.mjs`), exactly as for the toolbox.
27
+ //
28
+ // NOT THE ORCHESTRATOR. Part 7's separate package (E92–E102) ranks skills from measured evidence. This is
29
+ // the hand-made list that comes first: a person read each skill and wrote down why it fits.
30
+ import { createHash } from 'node:crypto';
31
+ import { c, emitJSON, fail, hand, info, log, warn } from './lib.mjs';
32
+ import { bindingsForProfile, loadSkillBindings, stepDef, stepSkills, STEPS } from './epic-state.mjs';
33
+ import { TOOLBOX, isCalendarDay, vettingProblems } from './toolbox.mjs';
34
+
35
+ // The list's own version and the day it was last read through as a whole. Move `version` whenever an
36
+ // entry is added, removed or changed — the test names the new digest to record beside it.
37
+ export const CATALOGUE = Object.freeze({ version: 1, checked: '2026-10-03' });
38
+
39
+ const VETTED_ON = '2026-10-03';
40
+
41
+ // ---- the list --------------------------------------------------------------------------------------
42
+ //
43
+ // step the step id it fits (a step a skill runs — never a review gate)
44
+ // skill the name to bind, as an agent calls it once the pool is installed
45
+ // pool the toolbox id of the pool it comes from (its install routes are the pool's)
46
+ // reason one sentence: what it adds before yadflow's own skill runs
47
+ // caveat null, or one sentence a team must know before binding it (an install profile, a file it writes)
48
+ // file the skill's SKILL.md in the pool's repository, at `vetted.release`
49
+ // licence + vetted: the E88 record, read for this entry
50
+ // The vetting record each pool's skills share, read on VETTED_ON at the pool's release tag: the licence
51
+ // from its LICENSE file, the source, and the npm package its installer is. Every SKILL.md below was
52
+ // fetched at that tag and its `name` read; none sets `disable-model-invocation`, so an agent can run each
53
+ // one as a step of a chain. Deprecated BMAD names that only forward to a newer skill (`bmad-create-prd`,
54
+ // `bmad-create-architecture`, `bmad-dev-story` — removed in BMAD v7) are left out on purpose.
55
+ const BMAD = Object.freeze({
56
+ on: VETTED_ON, licenceFrom: 'https://github.com/bmad-code-org/BMAD-METHOD/blob/main/LICENSE',
57
+ source: 'https://github.com/bmad-code-org/BMAD-METHOD', release: 'v6.12.0',
58
+ packages: [{ registry: 'npm', name: 'bmad-method', version: '6.12.0' }],
59
+ note: "GitHub reads the LICENSE as NOASSERTION: it is MIT text with a trademark notice added. Named as `npx bmad-method install` copies it (a plugin install calls it <plugin>:<name>).",
60
+ });
61
+ const ECC = Object.freeze({
62
+ on: VETTED_ON, licenceFrom: 'https://github.com/affaan-m/ECC/blob/main/LICENSE',
63
+ source: 'https://github.com/affaan-m/ECC', release: 'v2.2.3',
64
+ packages: [{ registry: 'npm', name: 'ecc-universal', version: '2.2.3' }],
65
+ note: 'Named as `npx ecc-universal install` copies it; the `ecc@ecc` plugin calls it ecc:<name>.',
66
+ });
67
+ const MATT = Object.freeze({
68
+ on: VETTED_ON, licenceFrom: 'https://github.com/mattpocock/skills/blob/main/LICENSE',
69
+ source: 'https://github.com/mattpocock/skills', release: 'v1.2.3',
70
+ packages: [],
71
+ note: 'Named as the mattpocock-skills plugin calls it (<plugin>:<name>): a plugin is the one install route the toolbox lists for this pool.',
72
+ });
73
+ const B = 'https://github.com/bmad-code-org/BMAD-METHOD/blob/v6.12.0/src';
74
+ const E = 'https://github.com/affaan-m/ECC/blob/v2.2.3/skills';
75
+ const M = 'https://github.com/mattpocock/skills/blob/v1.2.3/skills';
76
+ const ECC_PROFILE = (p) => `Not in ECC's smaller install profiles: install with --profile ${p}, or use the ecc@ecc plugin.`;
77
+
78
+ export const RECOMMENDATIONS = Object.freeze([
79
+ // ---- foundation: the product's purpose, scope and principles ----
80
+ { step: 'foundation', skill: 'bmad-prfaq', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-prfaq/SKILL.md`, vetted: BMAD, caveat: null,
81
+ reason: 'Works backwards from a press release and its hard questions, a sharp test of the product\'s purpose before the Foundation fixes it.' },
82
+ { step: 'foundation', skill: 'bmad-brainstorming', pool: 'bmad-method', licence: 'MIT', file: `${B}/core-skills/bmad-brainstorming/SKILL.md`, vetted: BMAD, caveat: null,
83
+ reason: 'Guided ideation with several techniques, so the options are wide before the principles are narrowed.' },
84
+ { step: 'foundation', skill: 'council', pool: 'ecc', licence: 'MIT', file: `${E}/council/SKILL.md`, vetted: ECC, caveat: null,
85
+ reason: 'A four-voice structured disagreement for the go/no-go and trade-off calls a Foundation records.' },
86
+
87
+ // ---- analysis: pressure-test the idea ----
88
+ { step: 'analysis', skill: 'bmad-forge-idea', pool: 'bmad-method', licence: 'MIT', file: `${B}/core-skills/bmad-forge-idea/SKILL.md`, vetted: BMAD, caveat: null,
89
+ reason: 'Several personas attack a half-formed idea, so the analysis starts from one that has already been tested.' },
90
+ { step: 'analysis', skill: 'bmad-product-brief', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-product-brief/SKILL.md`, vetted: BMAD, caveat: null,
91
+ reason: 'A structured product-brief template and validation pass to feed the discovery brief.' },
92
+
93
+ // ---- epic ----
94
+ { step: 'epic', skill: 'bmad-prd', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-prd/SKILL.md`, vetted: BMAD, caveat: null,
95
+ reason: 'BMAD\'s product-manager workflow for writing and validating a PRD, the closest counterpart of the epic.' },
96
+ { step: 'epic', skill: 'mattpocock-skills:grilling', pool: 'mattpocock-skills', licence: 'MIT', file: `${M}/productivity/grilling/SKILL.md`, vetted: MATT, caveat: null,
97
+ reason: 'Questions the plan hard before the product manager writes it down.' },
98
+
99
+ // ---- architecture: the design and the locked contract ----
100
+ { step: 'architecture', skill: 'bmad-architecture', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-architecture/SKILL.md`, vetted: BMAD, caveat: null,
101
+ reason: 'Records the decisions that keep separately built parts consistent, which is this step\'s cross-repo concern.' },
102
+ { step: 'architecture', skill: 'contract-first', pool: 'ecc', licence: 'MIT', file: `${E}/contract-first/SKILL.md`, vetted: ECC, caveat: ECC_PROFILE('developer'),
103
+ reason: 'Puts the contract in a machine-checkable form (OpenAPI, AsyncAPI, Protobuf or JSON Schema) before it is locked.' },
104
+ { step: 'architecture', skill: 'architecture-decision-records', pool: 'ecc', licence: 'MIT', file: `${E}/architecture-decision-records/SKILL.md`, vetted: ECC,
105
+ caveat: 'It writes numbered records under docs/adr/ as well.',
106
+ reason: 'Captures each architecture choice as a numbered decision record with the alternatives weighed.' },
107
+
108
+ // ---- ui-design ----
109
+ { step: 'ui-design', skill: 'bmad-ux', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-ux/SKILL.md`, vetted: BMAD,
110
+ caveat: 'It writes its own DESIGN.md, the file yad-ui writes last, so review that yad-ui kept what you want.',
111
+ reason: 'BMAD\'s UX designer: how the product looks and how it behaves, the direct counterpart of this step.' },
112
+ { step: 'ui-design', skill: 'accessibility', pool: 'ecc', licence: 'MIT', file: `${E}/accessibility/SKILL.md`, vetted: ECC, caveat: ECC_PROFILE('developer'),
113
+ reason: 'Adds a WCAG 2.2 AA pass for web, iOS and Android to the design.' },
114
+
115
+ // ---- stories ----
116
+ { step: 'stories', skill: 'bmad-create-epics-and-stories', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/plan/bmad-create-epics-and-stories/SKILL.md`, vetted: BMAD, caveat: null,
117
+ reason: 'BMAD\'s own breakdown of requirements into stories, as a first draft for yad-stories to file and tag by repo.' },
118
+
119
+ // ---- test-cases ----
120
+ { step: 'test-cases', skill: 'intent-driven-development', pool: 'ecc', licence: 'MIT', file: `${E}/intent-driven-development/SKILL.md`, vetted: ECC, caveat: null,
121
+ reason: 'Turns stories into scoped, checkable acceptance criteria with a way to verify each, the raw material of test cases.' },
122
+
123
+ // ---- implement: one small task ----
124
+ { step: 'implement', skill: 'mattpocock-skills:tdd', pool: 'mattpocock-skills', licence: 'MIT', file: `${M}/engineering/tdd/SKILL.md`, vetted: MATT, caveat: null,
125
+ reason: 'Red-green-refactor discipline inside the task\'s small diff.' },
126
+ { step: 'implement', skill: 'tdd-workflow', pool: 'ecc', licence: 'MIT', file: `${E}/tdd-workflow/SKILL.md`, vetted: ECC, caveat: null,
127
+ reason: 'Test-first work with a coverage target, for teams that want the number enforced as they go.' },
128
+ { step: 'implement', skill: 'bmad-build', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/ship/bmad-build/SKILL.md`, vetted: BMAD,
129
+ caveat: 'It aims to finish a whole story, so it can push past yad-implement\'s one task and three files; yad-implement still stops it there.',
130
+ reason: 'BMAD\'s official implementation method, with its own review and verification steps.' },
131
+
132
+ // ---- checks ----
133
+ { step: 'checks', skill: 'verification-loop', pool: 'ecc', licence: 'MIT', file: `${E}/verification-loop/SKILL.md`, vetted: ECC, caveat: null,
134
+ reason: 'Runs build, type check, lint, tests, a security search and a diff review locally, with a pass/fail report, before the CI gates run.' },
135
+
136
+ // ---- engineer-review: a first pass before the human review is recorded ----
137
+ { step: 'engineer-review', skill: 'bmad-code-review', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/ship/bmad-code-review/SKILL.md`, vetted: BMAD, caveat: null,
138
+ reason: 'Several independent reviewers in parallel, then sorted findings, as a first pass before a person reviews.' },
139
+ { step: 'engineer-review', skill: 'bmad-walkthrough', pool: 'bmad-method', licence: 'MIT', file: `${B}/bmm-skills/ship/bmad-walkthrough/SKILL.md`, vetted: BMAD, caveat: null,
140
+ reason: 'Walks the human reviewer through what the change is for, what to look at, and how to test it.' },
141
+ ]);
142
+
143
+ // ---- checking an entry ------------------------------------------------------------------------------
144
+
145
+ const POOLS = () => TOOLBOX.filter((t) => t.tier === 'recommended').map((t) => t.id);
146
+ const bindable = (step) => !!stepDef(step)?.skill && STEPS.some((s) => s.id === step);
147
+
148
+ // Every way an entry is malformed, as sentences; [] means it is well formed. The vetting record is
149
+ // checked by the toolbox's own `vettingProblems`, so one rule holds both lists.
150
+ export function recommendationProblems(r) {
151
+ const out = [];
152
+ const need = (ok, what) => { if (!ok) out.push(what); };
153
+ need(typeof r?.step === 'string' && bindable(r.step), 'step is a step a skill runs (not a review gate)');
154
+ need(typeof r?.skill === 'string' && /^[a-z0-9][a-z0-9:_-]*$/i.test(r.skill), 'skill is a skill name');
155
+ need(POOLS().includes(r?.pool), `pool is one of the toolbox's recommended pools (${POOLS().join(', ')})`);
156
+ need(typeof r?.reason === 'string' && r.reason.length > 0 && /\.$/.test(r.reason), 'reason is one sentence ending in a full stop');
157
+ need(typeof r?.file === 'string' && /^https:\/\/[^/\s]+\/.+\/SKILL\.md$/.test(r.file), 'file is the https URL of the skill\'s SKILL.md');
158
+ need(typeof r?.licence === 'string' && r.licence.length > 0, 'licence is set');
159
+ need(r?.caveat === null || (typeof r?.caveat === 'string' && /\.$/.test(r.caveat)), 'caveat is null or one sentence ending in a full stop');
160
+ for (const p of vettingProblems(r)) out.push(p);
161
+ // The file must be read at the release the record names, so the name was checked at that tag.
162
+ if (r?.vetted?.release && typeof r?.file === 'string') need(r.file.includes(`/${r.vetted.release}/`), 'file is read at vetted.release');
163
+ return out;
164
+ }
165
+
166
+ // An entry seen through the toolbox's vetting helpers, which name a record by `id` and `name`.
167
+ export const asVetted = (r) => ({ ...r, id: `${r.step}:${r.skill}`, name: `${r.skill} for ${r.step}` });
168
+
169
+ // A digest of the list's content, so a test can hold each version to exactly one list.
170
+ export const catalogueDigest = (list = RECOMMENDATIONS) =>
171
+ createHash('sha256').update(JSON.stringify(list)).digest('hex').slice(0, 16);
172
+
173
+ // How many whole days old the list is on `today` (a Date), counted in UTC like every record date.
174
+ export function catalogueAge(today = new Date(), checked = CATALOGUE.checked) {
175
+ if (!isCalendarDay(checked)) return null;
176
+ const day = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
177
+ return Math.floor((day - Date.parse(`${checked}T00:00:00Z`)) / 86400000);
178
+ }
179
+
180
+ // "version 1, checked 2026-10-03 (12 days ago)" — said wherever the list is shown.
181
+ export function catalogueLabel(today = new Date()) {
182
+ const age = catalogueAge(today);
183
+ const ago = age === null ? '' : age <= 0 ? ' (today)' : age === 1 ? ' (1 day ago)' : ` (${age} days ago)`;
184
+ return `recommendation catalogue version ${CATALOGUE.version}, checked ${CATALOGUE.checked}${ago}`;
185
+ }
186
+
187
+ // The recommendations for one step, in the list's order.
188
+ export const recommendationsFor = (step, list = RECOMMENDATIONS) => list.filter((r) => r.step === step);
189
+
190
+ // The chain a suggestion binds: the pool skill first, yadflow's own skill last (see the header).
191
+ export function suggestedChain(r) {
192
+ const own = stepDef(r.step)?.skill;
193
+ return own && own !== r.skill ? [r.skill, own] : [r.skill];
194
+ }
195
+
196
+ export function bindCommand(r, profile = null) {
197
+ return `yad skill bind ${r.step} ${suggestedChain(r).join(' ')}${profile ? ` --profile ${profile}` : ''}`;
198
+ }
199
+
200
+ // ---- yad skill recommend -----------------------------------------------------------------------------
201
+
202
+ const poolName = (id) => TOOLBOX.find((t) => t.id === id)?.name ?? id;
203
+
204
+ // Is the BOUND name this catalogue pick? A team that installed a pool as a plugin binds
205
+ // `ecc:contract-first`, and that is the catalogue's bare `contract-first`. Only that direction: a pick the
206
+ // catalogue names WITH its plugin (`mattpocock-skills:tdd`) is matched exactly, because a bare `tdd` is
207
+ // just as likely a team's own skill (review 2) — matching it would hide the bind command from a team that
208
+ // never ran the pool's skill. These are the plugin spellings `installedSkillNames` also accepts — including
209
+ // `<plugin>@<marketplace>:<skill>`, whose marketplace part is dropped before the exact comparison.
210
+ const bareName = (s) => String(s).split(':').at(-1);
211
+ const withoutMarketplace = (s) => String(s).replace(/^([^:@]+)@[^:]+:/, '$1:');
212
+ export const sameSkill = (bound, pick) => withoutMarketplace(bound) === pick || (!pick.includes(':') && bareName(bound) === pick);
213
+
214
+ // One row per recommendation: what it is, whether the step already runs it, whether it is installed here.
215
+ // `bindings` is the view for the route shown; `names` is `installedSkillNames`' answer (null: unknown).
216
+ // `onRoute(step)` is false for a step the route never walks — `yad skill bind --profile` refuses those, so
217
+ // a command for one would be a command that fails.
218
+ export function recommendRows({ step = null, bindings, names, list = RECOMMENDATIONS, profile = null, onRoute = () => true }) {
219
+ return list.filter((r) => (!step || r.step === step) && onRoute(r.step)).map((r) => {
220
+ const running = stepSkills(r.step, bindings);
221
+ const own = stepDef(r.step)?.skill;
222
+ return {
223
+ step: r.step, skill: r.skill, pool: r.pool, reason: r.reason, caveat: r.caveat, file: r.file, licence: r.licence,
224
+ bound: running.some((s) => sameSkill(s, r.skill)),
225
+ // Bound so that yadflow's own skill does not run LAST — in its place, or before it. Allowed (the
226
+ // file wins), never quiet: the last output of a chain is the artifact (closed decision 7), so the
227
+ // pool skill's output is then filed and yadflow's skill does not write it where the gate looks.
228
+ // yadflow's own skill is compared exactly: yadflow ships no plugin, so it has no `<plugin>:` spelling.
229
+ ownSkillDropped: running.some((s) => sameSkill(s, r.skill)) && !!own && running.at(-1) !== own,
230
+ installed: names ? names.has(r.skill) : null,
231
+ bind: suggestedChain(r),
232
+ command: bindCommand(r, profile),
233
+ vetted: r.vetted,
234
+ };
235
+ });
236
+ }
237
+
238
+ // The printer and --json answer of `yad skill recommend`. `yad skill` (cli/skill.mjs) works out the route
239
+ // and asks `yad detect` what is installed, then calls this.
240
+ export function showRecommendations(root, { step = null, json = false, route = { profile: null }, names = null, today = new Date(), onRoute = () => true } = {}) {
241
+ if (step !== null && step !== undefined && !stepDef(step)?.skill) {
242
+ // A review gate is a step, but no skill runs it, so nothing could be recommended for it — the same
243
+ // reason `yad skill bind` refuses one.
244
+ fail(stepDef(step) ? `\`${step}\` is a review gate — no skill runs it` : `\`${step}\` is not a step this yadflow runs`);
245
+ hand(`steps with a recommendation: ${[...new Set(RECOMMENDATIONS.map((r) => r.step))].join(' · ') || '(none yet)'}`);
246
+ process.exitCode = 1;
247
+ return undefined;
248
+ }
249
+ if (step && !onRoute(step)) {
250
+ fail(`route \`${route.profile}\` has no \`${step}\` step — nothing would ever run a binding there`);
251
+ hand(`see \`yad skill recommend --profile ${route.profile}\` for the steps it walks`);
252
+ process.exitCode = 1;
253
+ return undefined;
254
+ }
255
+ const bindings = bindingsForProfile(loadSkillBindings(root), route.profile);
256
+ const rows = recommendRows({ step, bindings, names, profile: route.profile, onRoute });
257
+ const catalogue = { version: CATALOGUE.version, checked: CATALOGUE.checked, age: catalogueAge(today) };
258
+
259
+ if (json) {
260
+ return emitJSON({
261
+ ok: true, catalogue, step: step ?? null, profile: route.profile, ...(route.epic ? { epic: route.epic } : {}),
262
+ installedChecked: !!names, recommendations: rows,
263
+ });
264
+ }
265
+
266
+ info(catalogueLabel(today));
267
+ if (route.profile) info(`route ${c.bold(route.profile)}${route.epic ? ` (${route.epic})` : ''} — the bind commands apply to it only`);
268
+ if (!rows.length) {
269
+ info(step ? `no recommendation for ${c.bold(step)} — yadflow's own skill (${stepDef(step)?.skill ?? 'none'}) is the one to run`
270
+ : route.profile ? `no recommendation for a step route ${route.profile} walks` : 'the catalogue is empty');
271
+ return { rows };
272
+ }
273
+ let last = null;
274
+ for (const r of rows) {
275
+ if (r.step !== last) {
276
+ log(`\n ${c.bold(r.step)} ${c.dim(`— runs ${stepSkills(r.step, bindings).join(' → ') || '(none)'} now`)}`);
277
+ last = r.step;
278
+ }
279
+ const mark = r.bound ? c.green('✓') : r.installed === false ? c.yellow('?') : '•';
280
+ log(` ${mark} ${c.bold(r.skill)} ${c.dim(`(${poolName(r.pool)}, ${r.licence})`)}`);
281
+ log(` ${r.reason}`);
282
+ if (r.caveat) log(` ${c.yellow('note:')} ${r.caveat}`);
283
+ log(` ${r.bound ? c.dim('already bound to this step') : c.cyan(r.command)}`);
284
+ if (r.ownSkillDropped) {
285
+ warn(`${r.step} runs ${r.skill} without ${stepDef(r.step).skill} last — the last skill's output is the artifact, so yadflow's skill does not file it where the gate looks (\`${r.command}\` chains them)`);
286
+ }
287
+ }
288
+ log('');
289
+ info('each one runs BEFORE yadflow\'s own skill, which still writes the artifact — every extra skill is another model run');
290
+ if (rows.some((r) => r.bound)) info(`${c.green('✓')} = this step already runs it`);
291
+ if (!names) warn('could not check which skills are installed here — `yad detect` could not read this folder or the home folder');
292
+ else if (rows.some((r) => r.installed === false && !r.bound)) {
293
+ info(`${c.yellow('?')} = not installed here — install its pool first (\`yad toolbox list\` shows how)`);
294
+ }
295
+ hand(`nothing is bound for you: run a command above to choose one, and \`yad skill unbind <step>${route.profile ? ` --profile ${route.profile}` : ''}\` to undo it`);
296
+ return { rows };
297
+ }
298
+
299
+ // Exported for `yad skill list`: the skills the catalogue recommends for a step (names only). Empty for a
300
+ // step the route never walks — `recommend` hides those too, and a bind for one is refused.
301
+ export const recommendedSkills = (step, onRoute = () => true) => (onRoute(step) ? recommendationsFor(step).map((r) => r.skill) : []);
package/cli/setup.mjs CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import { modeFields, modeOf } from './mode.mjs';
17
17
  import { recordActor } from './skip.mjs';
18
18
  import { loadSkillBindings, stepSkills } from './epic-state.mjs';
19
- import { offerToolbox } from './toolbox.mjs';
19
+ import { offerToolbox, REPOMIX_VERSION } from './toolbox.mjs';
20
20
 
21
21
  // Parse a comma/space separated list into a clean, deduped array of trimmed tokens.
22
22
  export function parseList(s) {
@@ -769,7 +769,8 @@ export function packRepo(root, repo) {
769
769
  fs.mkdirSync(path.dirname(out), { recursive: true });
770
770
  ensurePackIgnored(root); // keep the pack out of git before it is (re)written — see repo-publish.mjs invariant 1
771
771
  info(`${nm}: packing with repomix …`);
772
- const r = runLauncher('npx', ['repomix@latest', '--compress', '--include-logs', '--style', 'markdown', '-o', out], { cwd: repoRoot });
772
+ // The vetted version, never `@latest` (E88): a new Repomix release reaches users only once it is vetted.
773
+ const r = runLauncher('npx', [`repomix@${REPOMIX_VERSION}`, '--compress', '--include-logs', '--style', 'markdown', '-o', out], { cwd: repoRoot });
773
774
  if (r.ok) { ok(`${nm}: cached ${forTerminal(repo.contextPack)}`); hand(`${nm}: generate the code-map in your AI agent (yad-connect-repos)`); return true; }
774
775
  fail(`${nm}: repomix failed — ${forTerminal(r.stderr.split('\n')[0]) || 'unknown error'}`);
775
776
  return false;