yadflow 4.0.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ # [4.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.1.0...v4.2.0) (2026-10-01)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **toolbox:** a turned-off plugin is disabled, and ranges read only full versions (E84 review 1) ([56a9864](https://github.com/abdelrahmannasr/yadflow/commit/56a9864bf91b764b736c6da12b2d30cc1d1fd861))
7
+ * **toolbox:** add and remove report a shipped-tool edit through one function (E86 review 4) ([161ece4](https://github.com/abdelrahmannasr/yadflow/commit/161ece48cffd879fe0106747ae10698f7b5e67cf))
8
+ * **toolbox:** clearer remove and dead-copy wording (E86 review 5 notes) ([33ba0b7](https://github.com/abdelrahmannasr/yadflow/commit/33ba0b7bae11462d9b81a739df302fe20bbd2a7c))
9
+ * **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))
10
+ * **toolbox:** offer an undo only when the tool's use changed (E86 review 3) ([32e5105](https://github.com/abdelrahmannasr/yadflow/commit/32e5105bb83f856b9b37b8bc0e96e7bf15e396e1))
11
+ * **toolbox:** one remove clears everything under an id; add agrees with the list (E86 review 2) ([da75777](https://github.com/abdelrahmannasr/yadflow/commit/da75777496e4c705e52423c77c553f35e5c99002))
12
+ * **toolbox:** truthful edit messages, remove clears dead lines, --dir is the Product (E86 review 1) ([2159543](https://github.com/abdelrahmannasr/yadflow/commit/21595432d611e190c93d97fc707f27c432ba936f))
13
+
14
+
15
+ ### Features
16
+
17
+ * **toolbox:** yad toolbox add, remove and check (E86) ([715cd4c](https://github.com/abdelrahmannasr/yadflow/commit/715cd4c6cc63ffdcfdc33c97a9d2df8078965b64))
18
+ * **toolbox:** yad toolbox list and the shipped toolbox of external tools (E84) ([3f5374b](https://github.com/abdelrahmannasr/yadflow/commit/3f5374b830c83782b884f48a7e760943bc2349d4))
19
+
20
+ # [4.1.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.0.1...v4.1.0) (2026-10-01)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * **detect:** a non-object mcpServers is a problem in every file (E50 review 3) ([d917c72](https://github.com/abdelrahmannasr/yadflow/commit/d917c7229f98c61b5904659b82ece3cf13612ce9))
26
+ * **detect:** read Codex TOML keys only, keep plugins inside their folder (E50 review 1) ([be11fd2](https://github.com/abdelrahmannasr/yadflow/commit/be11fd2fcad238f994a2a68db609ddd8db2a0c8b))
27
+ * **detect:** report plugin MCP paths that leave or miss, keep lines in order (E50 review 4) ([84c0a6a](https://github.com/abdelrahmannasr/yadflow/commit/84c0a6ae44458ba78cbc4c418b1c307090150a8e))
28
+ * **detect:** report unreadable settings and a non-object mcpServers (E50 review 2) ([a4f7f20](https://github.com/abdelrahmannasr/yadflow/commit/a4f7f20ba5728bcd33ed4a2498026ab3ef6baa43))
29
+ * **detect:** zero-width names, and a plugin that names .mcp.json itself (E50 review 5) ([6704c9d](https://github.com/abdelrahmannasr/yadflow/commit/6704c9db3ea5a03db968445ec9eb5b36922c4c55))
30
+
31
+
32
+ ### Features
33
+
34
+ * **detect:** yad detect lists installed skills, agents, MCP servers and plugins (E50) ([ccd7142](https://github.com/abdelrahmannasr/yadflow/commit/ccd71428ac6e64624160b9b2cb867c4ec2522653))
35
+
1
36
  ## [4.0.1](https://github.com/abdelrahmannasr/yadflow/compare/v4.0.0...v4.0.1) (2026-09-30)
2
37
 
3
38
 
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
 
@@ -255,6 +256,8 @@ The record is worth trusting because people approve what goes into it.
255
256
  - the Shape and Build steps with review gates
256
257
  - linked changes and `yad thread`
257
258
  - `yad history`, `yad next` and `yad usage`
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
258
261
  - draft saving, claims and owners
259
262
  - the CI checks
260
263
  - the Review Companion
@@ -279,7 +282,8 @@ The record is worth trusting because people approve what goes into it.
279
282
  - [all 38 skills](docs/SKILLS.md)
280
283
  - [the by-hand walkthrough](docs/WALKTHROUGH.md)
281
284
  - **Upgrade:** [staying up to date](docs/CLI.md#staying-up-to-date) ·
282
- [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)
283
287
  - **Project:**
284
288
  - [CONTRIBUTING.md](CONTRIBUTING.md)
285
289
  - [SECURITY.md](SECURITY.md)
package/bin/commands.mjs CHANGED
@@ -35,6 +35,8 @@ export { runRepo } from '../cli/repo.mjs';
35
35
  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
+ export { runDetect } from '../cli/detect.mjs';
39
+ export { runToolboxAdd, runToolboxCheck, runToolboxList, runToolboxRemove } from '../cli/toolbox.mjs';
38
40
  export { runDoctor } from '../cli/doctor.mjs';
39
41
  export { runMigrate, warnIfProjectAhead } from '../cli/migrate.mjs';
40
42
  export { runNext } from '../cli/next.mjs';
package/bin/yad.mjs CHANGED
@@ -116,6 +116,22 @@ ${c.bold('Where am I / what next')}
116
116
  Bind a step to a skill of your own. Several skills run in
117
117
  the order given, one after another — each costs tokens
118
118
  yad skill unbind <step> Drop the binding; the step goes back to the engine's default
119
+ yad toolbox list [--json] [--dir <folder>]
120
+ The external tools yadflow can use — core (offered at setup),
121
+ recommended pools, the design/testing/learning connectors and
122
+ this project's own — whether each is found here, whether this
123
+ project uses it, and what yadflow does without it. Installs nothing
124
+ yad toolbox check [--json] The tools this project uses that are not ready here, each with
125
+ its install command and fallback. Never fails: none is required
126
+ yad toolbox add <id> Use a tool in this Product (.sdlc/toolbox.json); prints how to
127
+ install it, installs nothing. Your own tool: add <id> --custom
128
+ --role "<text>" --fallback "<text>" --detect <kind>:<name>[,…]
129
+ [--install "<type>: <command>"] [--source <https URL>]
130
+ yad toolbox remove <id> Stop using a tool in this Product, or drop one of your own
131
+ yad detect [--json] [--dir <folder>] Which skills, agents, MCP servers and plugins are installed
132
+ for the agents that work here — in this folder and in your
133
+ home folder, and which agents read each place. Read-only;
134
+ MCP servers are named, never their commands or settings
119
135
  yad skip <epic> <step> --reason <text> Mark an optional step N/A for this epic. Which steps
120
136
  those are comes from the epic's lifecycle route — on classic
121
137
  and analysis-first, ui-design: a backend/API/data epic with no
@@ -322,7 +338,7 @@ ${c.bold('Environment')}
322
338
  YAD_NO_REPORT=1 Never offer to file a bug report after a failure
323
339
  YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
324
340
 
325
- 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']);
341
+ 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']);
326
342
 
327
343
  function parseArgs(argv) {
328
344
  const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
@@ -369,6 +385,7 @@ function parseArgs(argv) {
369
385
  // The roadmap and the release check spell the default `--preview`. Accepting it means a script can
370
386
  // say what it means rather than relying on the absence of a flag.
371
387
  else if (a === '--preview') o.apply = false;
388
+ else if (a === '--custom') o.custom = true;
372
389
  else if (a === '--json') o.json = true;
373
390
  else if (a === '-h' || a === '--help') o.help = true;
374
391
  else if (a === '-v' || a === '--version') o.version = true;
@@ -418,6 +435,7 @@ const ACTIONS = {
418
435
  docs: { known: ['list', 'build', 'deploy', 'sync'], default: 'list' },
419
436
  reconcile: { known: ['check', 'refresh', 'wire'] },
420
437
  hook: { known: ['ledger-guard'] },
438
+ toolbox: { known: ['list', 'check', 'add', 'remove'], default: 'list' },
421
439
  };
422
440
  // E80 — how each command finds its folder. A PRODUCT command works on the Product; a REPO command works
423
441
  // on the code repo it runs in and reads the Product beside it. The rest take their folder as given:
@@ -553,12 +571,12 @@ async function main() {
553
571
  // A project written by a newer yadflow is warned about before any command reads it (docs/migrations/
554
572
  // shape-8.md). Not on `hook` — its stderr is the channel a block reason reaches a model on — and not
555
573
  // where the command reports the same thing itself (doctor, migrate) or runs before a project exists.
556
- if (!['hook', 'doctor', 'migrate', 'setup', 'report', 'new', 'init', 'join'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
574
+ if (!['hook', 'doctor', 'migrate', 'setup', 'report', 'detect', 'toolbox', 'new', 'init', 'join'].includes(cmd)) commands.warnIfProjectAhead(o.dir || process.cwd());
557
575
  // E122: the Product's settings (or an epic's PR ledger) under two names that say different things.
558
576
  // Every command refuses rather than pick one — whichever it picked, somebody's edit would be silently
559
577
  // ignored. Not `doctor` (it reports the drift), `migrate` (it ends it) or `report` (it files a bug
560
578
  // about a broken flow, and reads nothing it could get wrong). `hook` returned above: it guards.
561
- if (!['doctor', 'migrate', 'report'].includes(cmd)) {
579
+ if (!['doctor', 'migrate', 'report', 'detect', 'toolbox'].includes(cmd)) {
562
580
  // Both the folder the command runs on and, for a code-repo command, the Product it reads.
563
581
  for (const root of new Set([o.dir || process.cwd(), o.product].filter(Boolean))) {
564
582
  const drift = commands.productDriftError(root);
@@ -619,6 +637,56 @@ async function main() {
619
637
  case 'migrate':
620
638
  result = await commands.runMigrate(o.dir, { apply: o.apply, json: o.json, keep: o.keep ?? null });
621
639
  break;
640
+ case 'toolbox': {
641
+ // E84 + E86. Finding a tool reads the folder yad runs in (a code repo's own skills count); the
642
+ // team's choices are the Product's `.sdlc/toolbox.json`, found the way every Product command finds
643
+ // it. So `toolbox` is not in PRODUCT_CMDS: that would move the finding to the Product's folder.
644
+ const [, action = 'list', id, extra] = o._;
645
+ const usage = 'usage: yad toolbox list | check | add <id> [--custom …] | remove <id> [--json] [--dir <folder>]';
646
+ if (!['list', 'check', 'add', 'remove'].includes(action)) { refuse(`unknown toolbox action: ${action} (list, check, add, remove)`, usage); break; }
647
+ const found = commands.findProduct(dirGiven ? o.dir : shellCwd());
648
+ const rel = (p) => path.relative(process.cwd(), p) || '.';
649
+ if (action === 'list' || action === 'check') {
650
+ if (id) { refuse(`yad toolbox ${action} takes no more words (got: ${id})`, usage); break; }
651
+ // A reader may be pointed at a code repo with --dir: it finds tools there and reads the Product
652
+ // that repo belongs to. Why no Product was found is said, as every Product command says it —
653
+ // otherwise the team's choices would be missing from the answer without a word.
654
+ if (found?.problem) warn(`${found.problem} — showing the defaults`);
655
+ 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)}`);
656
+ const productRoot = found?.root ?? null;
657
+ const run = action === 'list' ? commands.runToolboxList : commands.runToolboxCheck;
658
+ result = run(o.dir || process.cwd(), { json: o.json, productRoot });
659
+ break;
660
+ }
661
+ if (extra) { refuse(`yad toolbox ${action} takes one tool id (got also: ${extra})`, usage); break; }
662
+ // A writer: the Product's file, never a code repo's, and the same refusals every writer has. With
663
+ // --dir it is the Product itself, as for every other Product command (`yad skill bind --dir`) —
664
+ // never a folder a walk up from it happens to reach.
665
+ const productRoot = dirGiven ? (found?.via === 'here' ? found.root : null) : (found?.root ?? null);
666
+ if (!productRoot) {
667
+ const near = found?.root ?? found?.elsewhere;
668
+ const where = dirGiven ? `with --dir, name the Product folder itself (the one holding .sdlc/product.json)${near ? `: --dir ${rel(near)}` : ''}`
669
+ : found?.elsewhere ? `the Product is ${rel(found.elsewhere)}: cd there, or pass --dir ${rel(found.elsewhere)}`
670
+ : `${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`;
671
+ refuse(`no Product here (${o.dir}) — yad toolbox ${action} writes the Product's .sdlc/toolbox.json`, where);
672
+ break;
673
+ }
674
+ if (found.via !== 'here') process.stderr.write(c.dim(`Product: ${rel(productRoot)}\n`));
675
+ commands.warnIfProjectAhead(productRoot);
676
+ const drift = commands.productDriftError(productRoot);
677
+ if (drift) throw drift;
678
+ result = action === 'add'
679
+ ? 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 })
680
+ : 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 });
681
+ break;
682
+ }
683
+ case 'detect': {
684
+ // E50. Reads the folder and the home folder; writes nothing.
685
+ const [, extra] = o._;
686
+ if (extra) { refuse(`yad detect takes no words (got: ${extra})`, 'usage: yad detect [--json] [--dir <folder>]'); break; }
687
+ result = commands.runDetect(o.dir || process.cwd(), { json: o.json });
688
+ break;
689
+ }
622
690
  case 'report':
623
691
  result = await commands.runReport(o.dir, { message: o.message });
624
692
  break;
@@ -990,7 +1058,9 @@ main()
990
1058
  // block reason travels on — an update banner there would land in front of a model.
991
1059
  // Resolved the way main() resolves it, NOT from argv[2]: that is the first raw argument, so
992
1060
  // `yad --dir <path> hook ledger-guard` puts `--dir` there and the banner slips through.
993
- if (parseArgs(process.argv.slice(2))._[0] !== 'hook') {
1061
+ // Nor `detect` (E50) or `toolbox` (E84): they promise no network and no file written, and the
1062
+ // check does both.
1063
+ if (!['hook', 'detect', 'toolbox'].includes(parseArgs(process.argv.slice(2))._[0])) {
994
1064
  const { maybeNotifyUpdate } = await import('./commands.mjs');
995
1065
  await maybeNotifyUpdate();
996
1066
  }