yadflow 4.2.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.
- package/CHANGELOG.md +54 -0
- package/README.md +2 -1
- package/bin/commands.mjs +2 -2
- package/bin/yad.mjs +31 -15
- package/cli/detect.mjs +2 -2
- package/cli/docs.mjs +1 -1
- package/cli/doctor.mjs +75 -3
- package/cli/epic-state.mjs +121 -24
- package/cli/epic.mjs +7 -5
- package/cli/find-product.mjs +24 -0
- package/cli/next.mjs +21 -9
- package/cli/recommend.mjs +301 -0
- package/cli/reconcile.mjs +19 -3
- package/cli/setup.mjs +14 -4
- package/cli/skill.mjs +263 -63
- package/cli/toolbox.mjs +479 -26
- package/cli/workspace.mjs +9 -1
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +1 -1
- package/skills/sdlc/module-help.csv +1 -1
- package/skills/yad-backfill/SKILL.md +40 -7
- package/skills/yad-backfill/references/backfill.md +5 -4
- package/skills/yad-change/SKILL.md +1 -1
- package/skills/yad-connect-design/SKILL.md +16 -0
- package/skills/yad-connect-learning/SKILL.md +14 -0
- package/skills/yad-connect-repos/SKILL.md +34 -5
- package/skills/yad-connect-repos/references/code-context.md +5 -5
- package/skills/yad-connect-testing/SKILL.md +20 -0
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-epic/references/state-schema.md +10 -2
- package/skills/yad-learn/SKILL.md +14 -0
- package/skills/yad-run/SKILL.md +7 -4
- package/skills/yad-run/references/run-loop.md +2 -1
- package/skills/yad-spec/SKILL.md +37 -6
- package/skills/yad-spec/references/spec-handoff.md +2 -2
- package/skills/yad-test-cases/SKILL.md +20 -0
- package/skills/yad-ui/SKILL.md +36 -3
package/cli/next.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { c, log, ok, info, warn, hand, fail, readJSON, exists, emitJSON } from '
|
|
|
15
15
|
import { PROJECT_FILES, isVerifiedLedger, productConfigPath, stepAdvance } from './manifest.mjs';
|
|
16
16
|
import { printTeamHint, soloTeamHint } from './people.mjs';
|
|
17
17
|
import { OWNER_STEPS, liveOwner, readState as loadLedgerState } from './owners.mjs';
|
|
18
|
-
import { dedupeConsecutive, epicIds, isGateStep, killSwitchOn, loadAutomation, epicRel, epicRoot, loadLedger, loadSkillBindings, stepSkills, nextAction, preconditionsMet, isValidEpicId, epicLineage, typeNoun, phaseOf, stepPhase, profileSteps, lifecycleProfile, PHASES, PRODUCT_DONE, PRODUCT_EPICS, STEPS } from './epic-state.mjs';
|
|
18
|
+
import { bindingsForEpic, bindingsForProfile, dedupeConsecutive, epicIds, isGateStep, killSwitchOn, loadAutomation, epicRel, epicRoot, loadLedger, loadSkillBindings, stepSkills, nextAction, preconditionsMet, isValidEpicId, epicLineage, typeNoun, phaseOf, stepPhase, profileSteps, lifecycleProfile, PHASES, PRODUCT_DONE, PRODUCT_EPICS, STEPS } from './epic-state.mjs';
|
|
19
19
|
|
|
20
20
|
// Is solo mode on? Persisted in hub.json by setup (Phase C/D); default false. Read defensively so a
|
|
21
21
|
// missing/old hub.json never breaks the driver.
|
|
@@ -46,10 +46,16 @@ function listEpics(root) {
|
|
|
46
46
|
// `bindings` is passed in for the same reason `lin` is: every loop below renders many epics, and the
|
|
47
47
|
// project's skill bindings are ONE file for all of them. The default keeps the single-epic callers a
|
|
48
48
|
// one-liner; a loop reads the file once and hands the same object to every row.
|
|
49
|
-
|
|
49
|
+
//
|
|
50
|
+
// E51: the bindings are narrowed to THIS epic's route here, once, and `rowFor` hands that same view to
|
|
51
|
+
// the printed line. `nextAction` would narrow them itself, but `actionLine` names skills the action
|
|
52
|
+
// never carried (the Build fallback, the hand-off to the next epic) — a view built in two places is two
|
|
53
|
+
// answers for one step.
|
|
54
|
+
function epicAction(root, id, lin = epicLineage(root, id), bindings = loadSkillBindings(root)) {
|
|
50
55
|
const ledger = loadLedger(epicRoot(root, id));
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
const view = bindingsForEpic(bindings, ledger?.state);
|
|
57
|
+
const action = {
|
|
58
|
+
...nextAction(ledger, { epic: id, bindings: view }),
|
|
53
59
|
lineageKind: lin.type,
|
|
54
60
|
// Which ROUTE this epic RECORDED, carried so `phaseLine` can mark a phase that route never enters
|
|
55
61
|
// (E40). The recorded key ONLY — never matched from the chain, which is the one place in this file
|
|
@@ -64,13 +70,19 @@ const actionFor = (root, id, lin = epicLineage(root, id), bindings = loadSkillBi
|
|
|
64
70
|
// which route it is on simply gets the line it got before the short lanes existed.
|
|
65
71
|
route: lifecycleProfile(ledger?.state?.profile) ? ledger.state.profile : null,
|
|
66
72
|
};
|
|
67
|
-
};
|
|
73
|
+
return { action, view };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const actionFor = (root, id, lin, bindings) => epicAction(root, id, lin, bindings).action;
|
|
68
77
|
|
|
69
78
|
// One `epic.md` read, both things that come out of it: the action to print and the tag to print
|
|
70
79
|
// beside it. Every printed path in this file goes through here.
|
|
80
|
+
// `bindings` on the row is the epic's own view, and every printer spreads the row AFTER its own
|
|
81
|
+
// `bindings`, so the row's wins.
|
|
71
82
|
const rowFor = (root, id, bindings = loadSkillBindings(root)) => {
|
|
72
83
|
const lin = epicLineage(root, id);
|
|
73
|
-
|
|
84
|
+
const { action, view } = epicAction(root, id, lin, bindings);
|
|
85
|
+
return { action, theme: lin.theme, owners: liveOwners(root, id), bindings: view };
|
|
74
86
|
};
|
|
75
87
|
|
|
76
88
|
// The live step owners of one epic (E47), by step. Beside the action, never in it: `yad next --json`'s
|
|
@@ -388,7 +400,7 @@ function generalNext(root, { all, headCount = null } = {}) {
|
|
|
388
400
|
if (brownfield) hand(`capture what already exists first: invoke the ${c.bold('yad-backfill')} skill`);
|
|
389
401
|
// Both name a STEP's skill, so both ask the project. `yad-backfill` above does not: waking a
|
|
390
402
|
// brownfield anchor is the engine's own promote verb, not a step on any chain.
|
|
391
|
-
if (!productId) hand(`frame the whole product first (purpose, scope, MVP, roadmap, stack): run ${c.bold('yad foundation new')}, then invoke the ${c.bold(stepSkills('foundation', bindings)[0] || 'yad-discovery')} skill ${c.dim('(optional)')}`);
|
|
403
|
+
if (!productId) hand(`frame the whole product first (purpose, scope, MVP, roadmap, stack): run ${c.bold('yad foundation new')}, then invoke the ${c.bold(stepSkills('foundation', bindingsForProfile(bindings, 'foundation'))[0] || 'yad-discovery')} skill ${c.dim('(optional)')}`);
|
|
392
404
|
hand(`start your first epic: invoke the ${c.bold(stepSkills('epic', bindings)[0] || 'yad-epic')} skill${productId ? c.dim(' (it reads the approved roadmap.md)') : ''}`);
|
|
393
405
|
return;
|
|
394
406
|
}
|
|
@@ -412,12 +424,12 @@ function generalNext(root, { all, headCount = null } = {}) {
|
|
|
412
424
|
// The grouping theme rides this list too. This is the ONE `yad next` view that shows several epics
|
|
413
425
|
// side by side, so it is where seeing which of them belong together is worth most — and leaving it
|
|
414
426
|
// off would have meant bare `yad next`, the command people run by default, never showed the tag.
|
|
415
|
-
for (const { action: a, theme: tag } of rows) {
|
|
427
|
+
for (const { action: a, theme: tag, bindings: view } of rows) {
|
|
416
428
|
const theme = tag ? ` ${c.dim(`#${tag}`)}` : '';
|
|
417
429
|
// Debt is reminded here too (E41): the roll-up is the view people run most, and a reminder that only
|
|
418
430
|
// appears once you already look at the one epic is not a reminder.
|
|
419
431
|
const owed = a.debt?.length ? ` ${c.yellow(`· ${a.debt.length} owed as debt`)}` : '';
|
|
420
|
-
log(` ${c.cyan(`${typeNoun(a.lineageKind)} ${a.epicId}`)}${theme} ${actionLine(a, { solo, verified })}${owed}`);
|
|
432
|
+
log(` ${c.cyan(`${typeNoun(a.lineageKind)} ${a.epicId}`)}${theme} ${actionLine(a, { solo, bindings: view, verified })}${owed}`);
|
|
421
433
|
}
|
|
422
434
|
// Painted per segment, not dim-wrapping a bold: `paint` closes with a full reset, so the nested
|
|
423
435
|
// form loses the dim from the first bold word to the end of the line.
|
|
@@ -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/reconcile.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { gitHead, packRepo } from './setup.mjs';
|
|
20
20
|
import { groupByRoot, commitUpdates, repoLabel } from './update-commit.mjs';
|
|
21
21
|
import { hasSiblingRepo, workspaceFileState, writeWorkspaceFile, WORKSPACE_FILE } from './find-product.mjs';
|
|
22
|
+
import { offerToolbox } from './toolbox.mjs';
|
|
22
23
|
|
|
23
24
|
const MARK = { missing: c.red('missing'), new: c.cyan('new'), outdated: c.yellow('outdated'), modified: c.cyan('modified'), stale: c.yellow('stale'), legacy: c.yellow('legacy'), removed: c.yellow('removed'), ok: c.green('ok') };
|
|
24
25
|
|
|
@@ -30,7 +31,7 @@ const repoShown = (repo) => `product (the code repo at ${forTerminal(repo.path)}
|
|
|
30
31
|
const shown = (a) => a.shownScope ?? a.scope;
|
|
31
32
|
const itemsOf = (actions) => actions.map((a) => ({ scope: a.scope, item: a.item, status: a.status, product: !a.fromRepo }));
|
|
32
33
|
|
|
33
|
-
export async function reconcile(root, { fix = false, scope = 'all', force = false, push = false, allowBranch = false, overwriteLocal = false } = {}) {
|
|
34
|
+
export async function reconcile(root, { fix = false, scope = 'all', force = false, push = false, allowBranch = false, overwriteLocal = false, toolbox: toolboxAt = null } = {}) {
|
|
34
35
|
log(c.bold(`\nSDLC reconcile ${c.dim('v' + VERSION)}`));
|
|
35
36
|
log(c.dim(`target: ${root}\n`));
|
|
36
37
|
|
|
@@ -239,7 +240,9 @@ export async function reconcile(root, { fix = false, scope = 'all', force = fals
|
|
|
239
240
|
if (workspaceFile === 'missing') info(`no ${WORKSPACE_FILE} beside the Product — \`yad check --fix\` writes it, so yad finds the Product from its code repos`);
|
|
240
241
|
if (push) warn('--push has no effect without --fix (there is nothing applied to commit).');
|
|
241
242
|
if (fixable.length || gaps.length) hand('run `yad check --fix` to reconcile (or `yad setup` for missing one-time setup).');
|
|
242
|
-
|
|
243
|
+
// After the --fix hint, never above it: --fix installs no tool, and must not read as if it did.
|
|
244
|
+
const toolbox = toolboxSection(root, toolboxAt);
|
|
245
|
+
return { fix: false, counts, gaps, items: itemsOf(actions), applied: 0, modified: modified.length, commits: [], workspaceFile, toolbox };
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
// --- apply --- (collect the applied actions so --push can stage each repo's exact allowlist) ---
|
|
@@ -314,5 +317,18 @@ export async function reconcile(root, { fix = false, scope = 'all', force = fals
|
|
|
314
317
|
},
|
|
315
318
|
});
|
|
316
319
|
}
|
|
317
|
-
|
|
320
|
+
const toolbox = toolboxSection(root, toolboxAt);
|
|
321
|
+
return { fix: true, counts, gaps, items: itemsOf(actions), applied, modified: modified.length, commits, workspaceFile, toolbox };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// E85: `yad check` and `yad update` end with the toolbox check — the tools this Product uses that are not
|
|
325
|
+
// here. Report only: it never adds to what `--fix` applies, never changes the exit code, and installs
|
|
326
|
+
// nothing. Both verbs, because both run here and a release can add a core tool. `at` comes from the
|
|
327
|
+
// command line: `from`, the top of the repo the person ran yad in, where tools are looked for (as
|
|
328
|
+
// `yad toolbox check` does — a code repo's own skills count); `product`, the Product whose choices are
|
|
329
|
+
// read, which is not `root` under `--dir <a code repo>`.
|
|
330
|
+
function toolboxSection(root, at) {
|
|
331
|
+
log('');
|
|
332
|
+
log(c.bold('Toolbox'));
|
|
333
|
+
return offerToolbox(at?.from || root, at?.product || root);
|
|
318
334
|
}
|
package/cli/setup.mjs
CHANGED
|
@@ -16,6 +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, REPOMIX_VERSION } from './toolbox.mjs';
|
|
19
20
|
|
|
20
21
|
// Parse a comma/space separated list into a clean, deduped array of trimmed tokens.
|
|
21
22
|
export function parseList(s) {
|
|
@@ -322,8 +323,9 @@ export async function runSetup(root, opts = {}) {
|
|
|
322
323
|
|
|
323
324
|
// 0. Profile interview — branch the wizard to the user's situation (solo/team, code, repo layout).
|
|
324
325
|
const { solo, team_size, codebase, repo_layout, configureTools } = await resolveProfile(root, opts);
|
|
325
|
-
// Steps: interview, preflight, install, Product, tools (1 if deferred else 3), repos, wire, coderabbit,
|
|
326
|
-
|
|
326
|
+
// Steps: interview, preflight, install, Product, tools (1 if deferred else 3), repos, wire, coderabbit,
|
|
327
|
+
// toolbox, done.
|
|
328
|
+
const total = 9 + (configureTools ? 3 : 1);
|
|
327
329
|
let _n = 0;
|
|
328
330
|
const S = (title) => step(++_n, total, title);
|
|
329
331
|
|
|
@@ -672,6 +674,13 @@ export async function runSetup(root, opts = {}) {
|
|
|
672
674
|
}
|
|
673
675
|
}
|
|
674
676
|
|
|
677
|
+
// E85: the external tools this project uses that are not here, and how to get each. AFTER the tools
|
|
678
|
+
// step, so a connector chosen there counts as in use. It offers and never installs: nothing is run and
|
|
679
|
+
// no file is written, and a missing tool never fails setup (each has a fallback).
|
|
680
|
+
S('Toolbox (external tools this project uses)');
|
|
681
|
+
guide(['Each tool is optional and has a fallback. yad installs nothing: it prints how to get each one, and you choose.']);
|
|
682
|
+
const toolbox = offerToolbox(root, root);
|
|
683
|
+
|
|
675
684
|
// Summary + version stamp
|
|
676
685
|
S('Done');
|
|
677
686
|
writeJSON(path.join(root, PROJECT_FILES.version), { version: VERSION, ideTargets, updatedAt: opts.today ?? null });
|
|
@@ -713,7 +722,7 @@ export async function runSetup(root, opts = {}) {
|
|
|
713
722
|
// step has written, so a scripted setup passes every flag (or SDLC_NONINTERACTIVE for the defaults).
|
|
714
723
|
return {
|
|
715
724
|
profile: { solo, team_size, codebase, repo_layout }, ideTargets,
|
|
716
|
-
repos: registry.repos.map((r) => r.name ?? null), next: epicSkill,
|
|
725
|
+
repos: registry.repos.map((r) => r.name ?? null), next: epicSkill, toolbox,
|
|
717
726
|
};
|
|
718
727
|
}
|
|
719
728
|
|
|
@@ -760,7 +769,8 @@ export function packRepo(root, repo) {
|
|
|
760
769
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
761
770
|
ensurePackIgnored(root); // keep the pack out of git before it is (re)written — see repo-publish.mjs invariant 1
|
|
762
771
|
info(`${nm}: packing with repomix …`);
|
|
763
|
-
|
|
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 });
|
|
764
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; }
|
|
765
775
|
fail(`${nm}: repomix failed — ${forTerminal(r.stderr.split('\n')[0]) || 'unknown error'}`);
|
|
766
776
|
return false;
|