yadflow 4.1.0 → 4.3.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 +34 -0
- package/README.md +5 -2
- package/bin/commands.mjs +2 -1
- package/bin/yad.mjs +71 -8
- package/cli/detect.mjs +1 -1
- package/cli/doctor.mjs +24 -0
- package/cli/errors.mjs +1 -0
- package/cli/find-product.mjs +24 -0
- package/cli/manifest.mjs +3 -0
- package/cli/reconcile.mjs +19 -3
- package/cli/setup.mjs +12 -3
- package/cli/toolbox.mjs +845 -0
- package/cli/workspace.mjs +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
# [4.3.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.2.0...v4.3.0) (2026-10-01)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **toolbox:** E85 review 1 — hint order, the folder check looks in, team-wide remove advice ([c7f6592](https://github.com/abdelrahmannasr/yadflow/commit/c7f65922cdad49e9816c4cbe81190283392c1df6))
|
|
7
|
+
* **toolbox:** E85 review 2 — look for tools at the top of the repo, read --dir's Product ([82fb5ed](https://github.com/abdelrahmannasr/yadflow/commit/82fb5ed6a1afcf364a9844d3faf2676bd4a84274))
|
|
8
|
+
* **toolbox:** E85 review 3 — a worktree is its own checkout; look at the deepest top ([4926799](https://github.com/abdelrahmannasr/yadflow/commit/4926799f6db758c7abb59029bd8e71923550c227))
|
|
9
|
+
* **toolbox:** E85 review 4 — find the checkout top under both spellings of a linked folder ([9728384](https://github.com/abdelrahmannasr/yadflow/commit/9728384c140178d55608d2ab2b67037a2f80140d))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **toolbox:** offer the missing tools in setup, check, update and join (E85) ([53e06c6](https://github.com/abdelrahmannasr/yadflow/commit/53e06c6da8bf585ced68435b052d01ba4ca5e6fa))
|
|
15
|
+
|
|
16
|
+
# [4.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.1.0...v4.2.0) (2026-10-01)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* **toolbox:** a turned-off plugin is disabled, and ranges read only full versions (E84 review 1) ([56a9864](https://github.com/abdelrahmannasr/yadflow/commit/56a9864bf91b764b736c6da12b2d30cc1d1fd861))
|
|
22
|
+
* **toolbox:** add and remove report a shipped-tool edit through one function (E86 review 4) ([161ece4](https://github.com/abdelrahmannasr/yadflow/commit/161ece48cffd879fe0106747ae10698f7b5e67cf))
|
|
23
|
+
* **toolbox:** clearer remove and dead-copy wording (E86 review 5 notes) ([33ba0b7](https://github.com/abdelrahmannasr/yadflow/commit/33ba0b7bae11462d9b81a739df302fe20bbd2a7c))
|
|
24
|
+
* **toolbox:** npx before a turned-off plugin, Claude Code switches only, Windows PATH rules (E84 review 2) ([f052979](https://github.com/abdelrahmannasr/yadflow/commit/f052979a3018790c1d7c94fe5b761ce32bf15e55))
|
|
25
|
+
* **toolbox:** offer an undo only when the tool's use changed (E86 review 3) ([32e5105](https://github.com/abdelrahmannasr/yadflow/commit/32e5105bb83f856b9b37b8bc0e96e7bf15e396e1))
|
|
26
|
+
* **toolbox:** one remove clears everything under an id; add agrees with the list (E86 review 2) ([da75777](https://github.com/abdelrahmannasr/yadflow/commit/da75777496e4c705e52423c77c553f35e5c99002))
|
|
27
|
+
* **toolbox:** truthful edit messages, remove clears dead lines, --dir is the Product (E86 review 1) ([2159543](https://github.com/abdelrahmannasr/yadflow/commit/21595432d611e190c93d97fc707f27c432ba936f))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
* **toolbox:** yad toolbox add, remove and check (E86) ([715cd4c](https://github.com/abdelrahmannasr/yadflow/commit/715cd4c6cc63ffdcfdc33c97a9d2df8078965b64))
|
|
33
|
+
* **toolbox:** yad toolbox list and the shipped toolbox of external tools (E84) ([3f5374b](https://github.com/abdelrahmannasr/yadflow/commit/3f5374b830c83782b884f48a7e760943bc2349d4))
|
|
34
|
+
|
|
1
35
|
# [4.1.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.0.1...v4.1.0) (2026-10-01)
|
|
2
36
|
|
|
3
37
|
|
package/README.md
CHANGED
|
@@ -155,7 +155,8 @@ unless CI runs it), or `yad gate advance` with no platform, is the human decisio
|
|
|
155
155
|
- `yad skip` and `yad defer` pass over optional steps, such as the UI design.
|
|
156
156
|
|
|
157
157
|
**Already using Yadflow?** Follow [staying up to date](docs/CLI.md#staying-up-to-date). Coming from
|
|
158
|
-
3.x, read [upgrading to 4.0](docs/migrations/upgrading-to-4.md) first
|
|
158
|
+
3.x, read [upgrading to 4.0](docs/migrations/upgrading-to-4.md) first, then follow the
|
|
159
|
+
[step-by-step upgrade guide](docs/migrations/upgrade-guide-3x-to-4x.md).
|
|
159
160
|
|
|
160
161
|
## How the project record evolves
|
|
161
162
|
|
|
@@ -256,6 +257,7 @@ The record is worth trusting because people approve what goes into it.
|
|
|
256
257
|
- linked changes and `yad thread`
|
|
257
258
|
- `yad history`, `yad next` and `yad usage`
|
|
258
259
|
- `yad detect`, which lists the skills, agents, MCP servers and plugins installed for each AI agent
|
|
260
|
+
- `yad toolbox list`, which shows the external tools yadflow can use, which ones you have, and what happens without each; `yad toolbox add` / `remove` record the tools your team uses, and `yad toolbox check` lists the ones not ready on your machine (`yad setup`, `check`, `update` and `join` print the same list, and never install anything)
|
|
259
261
|
- draft saving, claims and owners
|
|
260
262
|
- the CI checks
|
|
261
263
|
- the Review Companion
|
|
@@ -280,7 +282,8 @@ The record is worth trusting because people approve what goes into it.
|
|
|
280
282
|
- [all 38 skills](docs/SKILLS.md)
|
|
281
283
|
- [the by-hand walkthrough](docs/WALKTHROUGH.md)
|
|
282
284
|
- **Upgrade:** [staying up to date](docs/CLI.md#staying-up-to-date) ·
|
|
283
|
-
[upgrading to 4.0](docs/migrations/upgrading-to-4.md)
|
|
285
|
+
[upgrading to 4.0](docs/migrations/upgrading-to-4.md) ·
|
|
286
|
+
[step-by-step upgrade guide](docs/migrations/upgrade-guide-3x-to-4x.md)
|
|
284
287
|
- **Project:**
|
|
285
288
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
286
289
|
- [SECURITY.md](SECURITY.md)
|
package/bin/commands.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
export { runSetup } from '../cli/setup.mjs';
|
|
14
14
|
export { runNew, runInit, runJoin } from '../cli/workspace.mjs';
|
|
15
|
-
export { findProduct, hasProduct, workspaceProduct, registryEntryFor, WORKSPACE_FILE } from '../cli/find-product.mjs';
|
|
15
|
+
export { findProduct, hasProduct, workspaceProduct, registryEntryFor, toolsFolder, WORKSPACE_FILE } from '../cli/find-product.mjs';
|
|
16
16
|
export { reconcile } from '../cli/reconcile.mjs';
|
|
17
17
|
export { gateOpen, gateSync, gateComments, gateStatus, gateCi, gateReview, gateTrailer, gateWalkthrough, gateRepair } from '../cli/gate.mjs';
|
|
18
18
|
export { gateApprove, gateComment, gateAdvance } from '../cli/gate-local.mjs';
|
|
@@ -36,6 +36,7 @@ export { runRiskMap } from '../cli/riskmap-command.mjs';
|
|
|
36
36
|
export { runCodeowners } from '../cli/codeowners-command.mjs';
|
|
37
37
|
export { runDocs } from '../cli/docs.mjs';
|
|
38
38
|
export { runDetect } from '../cli/detect.mjs';
|
|
39
|
+
export { runToolboxAdd, runToolboxCheck, runToolboxList, runToolboxRemove } from '../cli/toolbox.mjs';
|
|
39
40
|
export { runDoctor } from '../cli/doctor.mjs';
|
|
40
41
|
export { runMigrate, warnIfProjectAhead } from '../cli/migrate.mjs';
|
|
41
42
|
export { runNext } from '../cli/next.mjs';
|
package/bin/yad.mjs
CHANGED
|
@@ -24,7 +24,8 @@ ${c.bold('Start a workspace')} ${c.dim('(the folder holding product/ and the cod
|
|
|
24
24
|
Never commits, pushes, or changes a shared file
|
|
25
25
|
|
|
26
26
|
${c.bold('Setup & maintenance')}
|
|
27
|
-
yad setup Guided first-run setup (profile interview, install, connect & wire repos
|
|
27
|
+
yad setup Guided first-run setup (profile interview, install, connect & wire repos,
|
|
28
|
+
then the tools this project uses that are missing — offered, never installed)
|
|
28
29
|
profile flags: --solo | --team <n>, --greenfield | --brownfield,
|
|
29
30
|
--monorepo | --separate, --tools (configure design/testing/learning now)
|
|
30
31
|
yad check Report what is missing / drifted / modified / stale / legacy (read-only)
|
|
@@ -116,6 +117,18 @@ ${c.bold('Where am I / what next')}
|
|
|
116
117
|
Bind a step to a skill of your own. Several skills run in
|
|
117
118
|
the order given, one after another — each costs tokens
|
|
118
119
|
yad skill unbind <step> Drop the binding; the step goes back to the engine's default
|
|
120
|
+
yad toolbox list [--json] [--dir <folder>]
|
|
121
|
+
The external tools yadflow can use — core (offered at setup),
|
|
122
|
+
recommended pools, the design/testing/learning connectors and
|
|
123
|
+
this project's own — whether each is found here, whether this
|
|
124
|
+
project uses it, and what yadflow does without it. Installs nothing
|
|
125
|
+
yad toolbox check [--json] The tools this project uses that are not ready here, each with
|
|
126
|
+
its install command and fallback. Never fails: none is required
|
|
127
|
+
yad toolbox add <id> Use a tool in this Product (.sdlc/toolbox.json); prints how to
|
|
128
|
+
install it, installs nothing. Your own tool: add <id> --custom
|
|
129
|
+
--role "<text>" --fallback "<text>" --detect <kind>:<name>[,…]
|
|
130
|
+
[--install "<type>: <command>"] [--source <https URL>]
|
|
131
|
+
yad toolbox remove <id> Stop using a tool in this Product, or drop one of your own
|
|
119
132
|
yad detect [--json] [--dir <folder>] Which skills, agents, MCP servers and plugins are installed
|
|
120
133
|
for the agents that work here — in this folder and in your
|
|
121
134
|
home folder, and which agents read each place. Read-only;
|
|
@@ -326,7 +339,7 @@ ${c.bold('Environment')}
|
|
|
326
339
|
YAD_NO_REPORT=1 Never offer to file a bug report after a failure
|
|
327
340
|
YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
|
|
328
341
|
|
|
329
|
-
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement', '--keep']);
|
|
342
|
+
const VALUE_FLAGS = new Set(['--dir', '--type', '--message', '--task', '--ai', '--risk', '--repo', '--platform', '--base', '--title', '--scope', '--branch', '--pr', '--epic', '--team', '--body', '--out', '--since', '--until', '--member', '--format', '--reason', '--profile', '--parent', '--inherits', '--to', '--retro-ship', '--merge-commit', '--path', '--ide-targets', '--theme', '--thread', '--by', '--count', '--engagement', '--keep', '--role', '--fallback', '--detect', '--install', '--source']);
|
|
330
343
|
|
|
331
344
|
function parseArgs(argv) {
|
|
332
345
|
const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
|
|
@@ -373,6 +386,7 @@ function parseArgs(argv) {
|
|
|
373
386
|
// The roadmap and the release check spell the default `--preview`. Accepting it means a script can
|
|
374
387
|
// say what it means rather than relying on the absence of a flag.
|
|
375
388
|
else if (a === '--preview') o.apply = false;
|
|
389
|
+
else if (a === '--custom') o.custom = true;
|
|
376
390
|
else if (a === '--json') o.json = true;
|
|
377
391
|
else if (a === '-h' || a === '--help') o.help = true;
|
|
378
392
|
else if (a === '-v' || a === '--version') o.version = true;
|
|
@@ -422,6 +436,7 @@ const ACTIONS = {
|
|
|
422
436
|
docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
|
|
423
437
|
reconcile: { known: ['check', 'refresh', 'wire'] },
|
|
424
438
|
hook: { known: ['ledger-guard'] },
|
|
439
|
+
toolbox: { known: ['list', 'check', 'add', 'remove'], default: 'list' },
|
|
425
440
|
};
|
|
426
441
|
// E80 — how each command finds its folder. A PRODUCT command works on the Product; a REPO command works
|
|
427
442
|
// on the code repo it runs in and reads the Product beside it. The rest take their folder as given:
|
|
@@ -525,8 +540,12 @@ async function main() {
|
|
|
525
540
|
// registers (through `.yad-workspace.json`) — and says where, on stderr. A code-repo command keeps acting
|
|
526
541
|
// on the repo it runs in, and is handed the Product to read (its registry: name, platform, branch).
|
|
527
542
|
const dirGiven = process.argv.slice(2).some((a) => a === '--dir' || a.startsWith('--dir='));
|
|
543
|
+
// E85: where `check` and `update` look for tools, and whose choices they read — worked out from the
|
|
544
|
+
// folder the person ran yad in (or named), before a Product command moves `o.dir` to the Product.
|
|
545
|
+
let toolboxAt = null;
|
|
528
546
|
if (PRODUCT_CMDS.has(cmd) || REPO_CMDS.has(cmd)) {
|
|
529
547
|
const found = commands.findProduct(dirGiven ? o.dir : shellCwd());
|
|
548
|
+
toolboxAt = { from: commands.toolsFolder(found, dirGiven ? o.dir : shellCwd()), product: found?.root ?? null };
|
|
530
549
|
const shown = (p) => path.relative(process.cwd(), p) || '.';
|
|
531
550
|
if (found?.problem) warn(`${found.problem} — using ${o.dir}`);
|
|
532
551
|
else if (found?.elsewhere) {
|
|
@@ -557,12 +576,12 @@ async function main() {
|
|
|
557
576
|
// A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
|
|
558
577
|
// shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
|
|
559
578
|
// where the command reports the same thing itself (doctor, migrate) or runs before a project exists.
|
|
560
|
-
if (!['hook', 'doctor', 'migrate', 'setup', 'report', 'detect', 'new', 'init', 'join'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
|
|
579
|
+
if (!['hook', 'doctor', 'migrate', 'setup', 'report', 'detect', 'toolbox', 'new', 'init', 'join'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
|
|
561
580
|
// E122: the Product's settings (or an epic's PR ledger) under two names that say different things.
|
|
562
581
|
// Every command refuses rather than pick one — whichever it picked, somebody's edit would be silently
|
|
563
582
|
// ignored. Not `doctor` (it reports the drift), `migrate` (it ends it) or `report` (it files a bug
|
|
564
583
|
// about a broken flow, and reads nothing it could get wrong). `hook` returned above: it guards.
|
|
565
|
-
if (!['doctor', 'migrate', 'report', 'detect'].includes(cmd)) {
|
|
584
|
+
if (!['doctor', 'migrate', 'report', 'detect', 'toolbox'].includes(cmd)) {
|
|
566
585
|
// Both the folder the command runs on and, for a code-repo command, the Product it reads.
|
|
567
586
|
for (const root of new Set([o.dir || process.cwd(), o.product].filter(Boolean))) {
|
|
568
587
|
const drift = commands.productDriftError(root);
|
|
@@ -611,11 +630,11 @@ async function main() {
|
|
|
611
630
|
}
|
|
612
631
|
case 'check':
|
|
613
632
|
if (noProduct()) break;
|
|
614
|
-
result = await commands.reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
633
|
+
result = await commands.reconcile(o.dir, { fix: o.fix, scope: o.scope, force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today, toolbox: toolboxAt });
|
|
615
634
|
break;
|
|
616
635
|
case 'update':
|
|
617
636
|
if (noProduct()) break;
|
|
618
|
-
result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today });
|
|
637
|
+
result = await commands.reconcile(o.dir, { fix: true, scope: 'changed', force: o.force, push: o.push, allowBranch: o.allowBranch, overwriteLocal: o.overwriteLocal, today, toolbox: toolboxAt });
|
|
619
638
|
break;
|
|
620
639
|
case 'doctor':
|
|
621
640
|
result = await commands.runDoctor(o.dir, { json: o.json });
|
|
@@ -623,6 +642,49 @@ async function main() {
|
|
|
623
642
|
case 'migrate':
|
|
624
643
|
result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json, keep: o.keep ?? null });
|
|
625
644
|
break;
|
|
645
|
+
case 'toolbox': {
|
|
646
|
+
// E84 + E86. Finding a tool reads the folder yad runs in (a code repo's own skills count); the
|
|
647
|
+
// team's choices are the Product's `.sdlc/toolbox.json`, found the way every Product command finds
|
|
648
|
+
// it. So `toolbox` is not in PRODUCT_CMDS: that would move the finding to the Product's folder.
|
|
649
|
+
const [, action = 'list', id, extra] = o._;
|
|
650
|
+
const usage = 'usage: yad toolbox list | check | add <id> [--custom …] | remove <id> [--json] [--dir <folder>]';
|
|
651
|
+
if (!['list', 'check', 'add', 'remove'].includes(action)) { refuse(`unknown toolbox action: ${action} (list, check, add, remove)`, usage); break; }
|
|
652
|
+
const found = commands.findProduct(dirGiven ? o.dir : shellCwd());
|
|
653
|
+
const rel = (p) => path.relative(process.cwd(), p) || '.';
|
|
654
|
+
if (action === 'list' || action === 'check') {
|
|
655
|
+
if (id) { refuse(`yad toolbox ${action} takes no more words (got: ${id})`, usage); break; }
|
|
656
|
+
// A reader may be pointed at a code repo with --dir: it finds tools there and reads the Product
|
|
657
|
+
// that repo belongs to. Why no Product was found is said, as every Product command says it —
|
|
658
|
+
// otherwise the team's choices would be missing from the answer without a word.
|
|
659
|
+
if (found?.problem) warn(`${found.problem} — showing the defaults`);
|
|
660
|
+
else if (found?.elsewhere) warn(`this folder is not in a repo the Product registers — the Product is ${rel(found.elsewhere)}: cd there, or pass --dir ${rel(found.elsewhere)}`);
|
|
661
|
+
const productRoot = found?.root ?? null;
|
|
662
|
+
const run = action === 'list' ? commands.runToolboxList : commands.runToolboxCheck;
|
|
663
|
+
result = run(commands.toolsFolder(found, o.dir || process.cwd()), { json: o.json, productRoot });
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
if (extra) { refuse(`yad toolbox ${action} takes one tool id (got also: ${extra})`, usage); break; }
|
|
667
|
+
// A writer: the Product's file, never a code repo's, and the same refusals every writer has. With
|
|
668
|
+
// --dir it is the Product itself, as for every other Product command (`yad skill bind --dir`) —
|
|
669
|
+
// never a folder a walk up from it happens to reach.
|
|
670
|
+
const productRoot = dirGiven ? (found?.via === 'here' ? found.root : null) : (found?.root ?? null);
|
|
671
|
+
if (!productRoot) {
|
|
672
|
+
const near = found?.root ?? found?.elsewhere;
|
|
673
|
+
const where = dirGiven ? `with --dir, name the Product folder itself (the one holding .sdlc/product.json)${near ? `: --dir ${rel(near)}` : ''}`
|
|
674
|
+
: found?.elsewhere ? `the Product is ${rel(found.elsewhere)}: cd there, or pass --dir ${rel(found.elsewhere)}`
|
|
675
|
+
: `${found?.problem ? `${found.problem}. ` : ''}run it from the Product, from inside a repo it registers, or pass --dir <the Product>; \`yad new\` / \`yad init\` start one`;
|
|
676
|
+
refuse(`no Product here (${o.dir}) — yad toolbox ${action} writes the Product's .sdlc/toolbox.json`, where);
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
if (found.via !== 'here') process.stderr.write(c.dim(`Product: ${rel(productRoot)}\n`));
|
|
680
|
+
commands.warnIfProjectAhead(productRoot);
|
|
681
|
+
const drift = commands.productDriftError(productRoot);
|
|
682
|
+
if (drift) throw drift;
|
|
683
|
+
result = action === 'add'
|
|
684
|
+
? commands.runToolboxAdd(productRoot, { id, custom: o.custom, role: o.role, fallback: o.fallback, detect: o.detect, install: o.install, source: o.source, json: o.json })
|
|
685
|
+
: commands.runToolboxRemove(productRoot, { id, custom: o.custom, role: o.role, fallback: o.fallback, detect: o.detect, install: o.install, source: o.source, json: o.json });
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
626
688
|
case 'detect': {
|
|
627
689
|
// E50. Reads the folder and the home folder; writes nothing.
|
|
628
690
|
const [, extra] = o._;
|
|
@@ -1001,8 +1063,9 @@ main()
|
|
|
1001
1063
|
// block reason travels on — an update banner there would land in front of a model.
|
|
1002
1064
|
// Resolved the way main() resolves it, NOT from argv[2]: that is the first raw argument, so
|
|
1003
1065
|
// `yad --dir <path> hook ledger-guard` puts `--dir` there and the banner slips through.
|
|
1004
|
-
// Nor `detect` (E50):
|
|
1005
|
-
|
|
1066
|
+
// Nor `detect` (E50) or `toolbox` (E84): they promise no network and no file written, and the
|
|
1067
|
+
// check does both.
|
|
1068
|
+
if (!['hook', 'detect', 'toolbox'].includes(parseArgs(process.argv.slice(2))._[0])) {
|
|
1006
1069
|
const { maybeNotifyUpdate } = await import('./commands.mjs');
|
|
1007
1070
|
await maybeNotifyUpdate();
|
|
1008
1071
|
}
|
package/cli/detect.mjs
CHANGED
|
@@ -163,7 +163,7 @@ const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
163
163
|
// `--json` escapes only U+0000–001F and hands the rest to a program unchanged.
|
|
164
164
|
// eslint-disable-next-line no-control-regex -- matching control characters is the point
|
|
165
165
|
const UNSAFE = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028\u2029\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/g;
|
|
166
|
-
const clean = (v) => String(v).replace(UNSAFE, '?');
|
|
166
|
+
export const clean = (v) => String(v).replace(UNSAFE, '?');
|
|
167
167
|
|
|
168
168
|
// ---- the three file formats -------------------------------------------------------------------------
|
|
169
169
|
|
package/cli/doctor.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { soloTeamHint, TEAM_CMD } from './people.mjs';
|
|
|
25
25
|
import { indexFreshness, INDEX_FILE } from './product-index.mjs';
|
|
26
26
|
import { productGit, resolveDefaultBranch } from './productcommit.mjs';
|
|
27
27
|
import { readOwners } from './owners.mjs';
|
|
28
|
+
import { loadChoices, readToolboxFile } from './toolbox.mjs';
|
|
28
29
|
|
|
29
30
|
// A registered path doctor may run git in (E81): a checkout the judgement accepts — never a refused
|
|
30
31
|
// entry (git run in a folder the Product shaped as a repo reads its `config`, which can run a command),
|
|
@@ -446,6 +447,7 @@ export function projectChecks(checks, root, { headCount = null } = {}) {
|
|
|
446
447
|
// inside the `project` block. The renderer prints a header every time the section CHANGES, so a
|
|
447
448
|
// `project` check added after the `shape` section prints the word "project" a second time.
|
|
448
449
|
skillBindingChecks(checks, root);
|
|
450
|
+
toolboxFileChecks(checks, root);
|
|
449
451
|
|
|
450
452
|
// repos.json: parse + every entry is a live git repo; staleness vs syncedHead
|
|
451
453
|
let registry = { repos: [] };
|
|
@@ -2107,6 +2109,28 @@ export function skillBindingChecks(checks, root) {
|
|
|
2107
2109
|
}
|
|
2108
2110
|
}
|
|
2109
2111
|
|
|
2112
|
+
// `.sdlc/toolbox.json`: which toolbox tools this Product uses or skips, and its own tools (E86). Absent is
|
|
2113
|
+
// the normal case. THE FILE ONLY: whether a tool is installed is `yad toolbox check`'s answer, and a
|
|
2114
|
+
// missing tool is never a doctor line (decided with E86 — no tool is required). A broken file fails, as
|
|
2115
|
+
// skills.json does; a line that does nothing warns, because `yad toolbox list` quietly shows the default.
|
|
2116
|
+
export function toolboxFileChecks(checks, root) {
|
|
2117
|
+
const rel = PROJECT_FILES.toolboxConfig;
|
|
2118
|
+
if (!exists(path.join(root, rel))) return;
|
|
2119
|
+
const { error } = readToolboxFile(root);
|
|
2120
|
+
if (error) {
|
|
2121
|
+
check(checks, 'toolbox', 'project', 'fail', `${rel} ${error}`, 'fix the JSON or restore it from git — `yad toolbox list` shows the defaults until then');
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
const { problems, unreachable, shipped, custom } = loadChoices(root);
|
|
2125
|
+
if (problems.length) {
|
|
2126
|
+
// An entry with no id (or an empty one) is out of `remove`'s reach; only hand-editing fixes it.
|
|
2127
|
+
check(checks, 'toolbox', 'project', 'warn', `${rel}: ${problems.length} line(s) do nothing — ${problems.map((p) => p.replace(`${rel}: `, '')).join('; ')} [YAD-CFG-007]`,
|
|
2128
|
+
`fix them by hand, or \`yad toolbox remove <id>\` — it clears whatever the file holds under that id — and add the tool again${unreachable ? ' (an entry with no id, or an empty one: fix it by hand)' : ''}`);
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
check(checks, 'toolbox', 'project', 'ok', `toolbox: ${shipped.size} shipped tool choice(s), ${custom.length} tool(s) of this project's own`);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2110
2134
|
// A step this release does not recognise. Every step the engine can run has a row in the step
|
|
2111
2135
|
// catalogue (E4), and that row is what gives it both a phase and a skill. So an id no phase claims is
|
|
2112
2136
|
// an id with no row at all: one `yad next` cannot guide, `yad gate` has no artifact rule for, and no
|
package/cli/errors.mjs
CHANGED
|
@@ -30,6 +30,7 @@ export const CODES = {
|
|
|
30
30
|
'YAD-CFG-004': 'learning.json names an unknown learning tool (expected one of config.yaml learning.tools, or none)',
|
|
31
31
|
'YAD-CFG-005': 'the Product settings (product.json, or hub.json on a Product that has only that name) set a platform but are missing git_url (required to scope auth + open PRs)',
|
|
32
32
|
'YAD-CFG-006': 'skills.json binds a step to something that is not a skill name (expected a string, or a non-empty list of strings)',
|
|
33
|
+
'YAD-CFG-007': 'toolbox.json has a line that does nothing — a tool this yadflow does not ship, a choice other than use or skip, or a custom tool that is incomplete, malformed, listed twice or uses a shipped tool\'s id',
|
|
33
34
|
'YAD-CLI-001': 'a --json run needed an answer that only a prompt could give',
|
|
34
35
|
};
|
|
35
36
|
|
package/cli/find-product.mjs
CHANGED
|
@@ -181,6 +181,30 @@ export function gitTopOf(dir) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
// Where `yad toolbox` and the toolbox section of `check` / `update` look for tools (E85), given
|
|
185
|
+
// `findProduct(from)`'s answer. Finding reads `<folder>/.claude/…` and never walks up, so a subfolder
|
|
186
|
+
// must be lifted to the top of what the person is working in. That is the DEEPEST of these that holds
|
|
187
|
+
// `from`: the git checkout it is in (a worktree inside `api/` is its own checkout, on its own branch — never
|
|
188
|
+
// swapped for api's, as in E80 review 3), the registered repo holding it (`apps/web` in a monorepo is not
|
|
189
|
+
// a checkout of its own), and the Product found from it. A checkout ABOVE the Product (a Product that is
|
|
190
|
+
// not a git repo, inside one that is not ours) is never deeper than the Product, so it never wins. Reads
|
|
191
|
+
// only whether `.git` exists; runs nothing.
|
|
192
|
+
export function toolsFolder(found, from) {
|
|
193
|
+
const at = real(from);
|
|
194
|
+
if (!at) return from;
|
|
195
|
+
// Both spellings: the shell's (through a link) and the one on disk, so `check` (given the shell's
|
|
196
|
+
// folder) and `yad toolbox check` (given the process's) look in the same place (E85 review 4).
|
|
197
|
+
const candidates = [gitTopOf(from), gitTopOf(at)];
|
|
198
|
+
if (found?.root && found.repo && typeof found.repo.path === 'string') candidates.push(path.resolve(found.root, found.repo.path));
|
|
199
|
+
if (found?.root && (found.via === 'here' || found.via === 'above')) candidates.push(found.root);
|
|
200
|
+
let best = null;
|
|
201
|
+
for (const c of candidates) {
|
|
202
|
+
const cAt = c && real(c);
|
|
203
|
+
if (cAt && within(at, cAt) && (!best || cAt.length > best.length)) best = cAt;
|
|
204
|
+
}
|
|
205
|
+
return best ?? from;
|
|
206
|
+
}
|
|
207
|
+
|
|
184
208
|
// The registry entry for the code repo a code-repo command runs in (E80), with the folder to work in.
|
|
185
209
|
// It is the CHECKOUT that must be the registered repo — not merely a folder inside it: a worktree or a
|
|
186
210
|
// repo nested in `backend/` is another checkout, on another branch, and must never be swapped for
|
package/cli/manifest.mjs
CHANGED
|
@@ -275,6 +275,9 @@ export const PROJECT_FILES = {
|
|
|
275
275
|
// the normal case and means "every step uses the skill the catalogue names" — like design.json,
|
|
276
276
|
// which is absent on a markdown-only project.
|
|
277
277
|
skillsConfig: '.sdlc/skills.json',
|
|
278
|
+
// Which toolbox tools this project uses or skips, and its own tools (E86). Absent is the normal case:
|
|
279
|
+
// the core tools and the connected design, testing and learning tools are the ones in use.
|
|
280
|
+
toolboxConfig: '.sdlc/toolbox.json',
|
|
278
281
|
// The kill switch and the Shape steps a team has set to `advance: auto` (E34). Absent is the normal
|
|
279
282
|
// case: the switch is off and every Shape step is `human`. A project file, not the module config the
|
|
280
283
|
// switch lived in before (`_bmad/sdlc/config.yaml` then, `.sdlc/config.yaml` since E3) — that one is
|
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 } 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
|
|