yadflow 4.2.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 CHANGED
@@ -1,3 +1,18 @@
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
+
1
16
  # [4.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.1.0...v4.2.0) (2026-10-01)
2
17
 
3
18
 
package/README.md CHANGED
@@ -257,7 +257,7 @@ The record is worth trusting because people approve what goes into it.
257
257
  - linked changes and `yad thread`
258
258
  - `yad history`, `yad next` and `yad usage`
259
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
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)
261
261
  - draft saving, claims and owners
262
262
  - the CI checks
263
263
  - the Review Companion
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';
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)
@@ -539,8 +540,12 @@ async function main() {
539
540
  // registers (through `.yad-workspace.json`) — and says where, on stderr. A code-repo command keeps acting
540
541
  // on the repo it runs in, and is handed the Product to read (its registry: name, platform, branch).
541
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;
542
546
  if (PRODUCT_CMDS.has(cmd) || REPO_CMDS.has(cmd)) {
543
547
  const found = commands.findProduct(dirGiven ? o.dir : shellCwd());
548
+ toolboxAt = { from: commands.toolsFolder(found, dirGiven ? o.dir : shellCwd()), product: found?.root ?? null };
544
549
  const shown = (p) => path.relative(process.cwd(), p) || '.';
545
550
  if (found?.problem) warn(`${found.problem} — using ${o.dir}`);
546
551
  else if (found?.elsewhere) {
@@ -625,11 +630,11 @@ async function main() {
625
630
  }
626
631
  case 'check':
627
632
  if (noProduct()) break;
628
- 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 });
629
634
  break;
630
635
  case 'update':
631
636
  if (noProduct()) break;
632
- 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 });
633
638
  break;
634
639
  case 'doctor':
635
640
  result = await commands.runDoctor(o.dir, { json: o.json });
@@ -655,7 +660,7 @@ async function main() {
655
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)}`);
656
661
  const productRoot = found?.root ?? null;
657
662
  const run = action === 'list' ? commands.runToolboxList : commands.runToolboxCheck;
658
- result = run(o.dir || process.cwd(), { json: o.json, productRoot });
663
+ result = run(commands.toolsFolder(found, o.dir || process.cwd()), { json: o.json, productRoot });
659
664
  break;
660
665
  }
661
666
  if (extra) { refuse(`yad toolbox ${action} takes one tool id (got also: ${extra})`, usage); break; }
@@ -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/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
- return { fix: false, counts, gaps, items: itemsOf(actions), applied: 0, modified: modified.length, commits: [], workspaceFile };
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
- return { fix: true, counts, gaps, items: itemsOf(actions), applied, modified: modified.length, commits, workspaceFile };
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, done.
326
- const total = 8 + (configureTools ? 3 : 1);
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
 
package/cli/toolbox.mjs CHANGED
@@ -604,30 +604,61 @@ export function toolboxCheck(root, productRoot, opts = {}) {
604
604
  return { used, findings, problems };
605
605
  }
606
606
 
607
- // NEVER FAILS (decided with the row): no tool is required, so a missing one is news, not an error. Exit 0.
608
- export function runToolboxCheck(root, { json = false, productRoot = null, ...opts } = {}) {
609
- const { used, findings, problems } = toolboxCheck(root, productRoot, opts);
610
- if (json) return emitJSON({ ok: true, ...(productRoot ? where(productRoot) : { product: null, file: null }), used: used.map((r) => r.id), findings, problems });
611
- for (const p of problems) warn(p);
612
- if (!findings.length) {
613
- ok(`all ${used.length} tool(s) this project uses are here: ${used.map((r) => clean(r.name)).join(', ')}`);
614
- return undefined;
615
- }
616
- log(c.bold(`${findings.length} of the ${used.length} tool(s) this project uses ${findings.length === 1 ? 'is' : 'are'} not ready here. None is required.`));
607
+ // The lines a check prints, from `toolboxCheck`'s answer, without the closing hint. One printer for
608
+ // `yad toolbox check` and for the toolbox step of setup, update, check and join (E85), so they cannot
609
+ // say it differently.
610
+ export function toolboxCheckLines({ used, findings }) {
611
+ if (!used.length) return [` ${c.dim('•')} this project uses no tool from the toolbox — \`yad toolbox list\` shows them all`];
612
+ if (!findings.length) return [` ${c.green('✓')} all ${used.length} tool(s) this project uses are here: ${used.map((r) => clean(r.name)).join(', ')}`];
613
+ const lines = [c.bold(`${findings.length} of the ${used.length} tool(s) this project uses ${findings.length === 1 ? 'is' : 'are'} not ready here. None is required.`)];
617
614
  for (const f of findings) {
618
615
  const r = used.find((u) => u.id === f.id);
619
616
  const what = f.problem === 'missing' ? 'not found'
620
617
  : f.problem === 'disabled' ? 'installed, but its plugin is turned off in your Claude Code settings'
621
618
  : `version ${clean(f.version)} is outside the known-good range ${f.versions} — still used`;
622
- log(` ${f.problem === 'out-of-range' ? c.yellow('!') : c.dim('–')} ${c.bold(clean(f.name))}: ${what}`);
623
- if (f.problem === 'disabled') log(` ${c.dim('turn it on with /plugin in Claude Code')}`);
624
- else for (const line of howToGet(r)) log(` ${c.dim(line)}`);
625
- if (f.problem !== 'out-of-range') log(` ${c.dim(`without it: ${clean(f.fallback)}`)}`);
619
+ lines.push(` ${f.problem === 'out-of-range' ? c.yellow('!') : c.dim('–')} ${c.bold(clean(f.name))}: ${what}`);
620
+ if (f.problem === 'disabled') lines.push(` ${c.dim('turn it on with /plugin in Claude Code')}`);
621
+ else for (const line of howToGet(r)) lines.push(` ${c.dim(line)}`);
622
+ if (f.problem !== 'out-of-range') lines.push(` ${c.dim(`without it: ${clean(f.fallback)}`)}`);
626
623
  }
627
- info(productRoot ? 'stop using a tool here with `yad toolbox remove <id>`' : 'no Product here, so this checks the core tools only');
624
+ return lines;
625
+ }
626
+
627
+ // NEVER FAILS (decided with the row): no tool is required, so a missing one is news, not an error. Exit 0.
628
+ export function runToolboxCheck(root, { json = false, productRoot = null, ...opts } = {}) {
629
+ const res = toolboxCheck(root, productRoot, opts);
630
+ const { used, findings, problems } = res;
631
+ if (json) return emitJSON({ ok: true, ...(productRoot ? where(productRoot) : { product: null, file: null }), used: used.map((r) => r.id), findings, problems });
632
+ for (const p of problems) warn(p);
633
+ for (const line of toolboxCheckLines(res)) log(line);
634
+ if (findings.length) info(productRoot ? 'stop using a tool, for the whole team, with `yad toolbox remove <id>`' : 'no Product here, so this checks the core tools only');
628
635
  return undefined;
629
636
  }
630
637
 
638
+ // The toolbox step of `yad setup`, `yad check`, `yad update` and `yad join` (E85). It OFFERS: it prints
639
+ // what the project uses that is not here and how to get it. It installs nothing, runs no program and
640
+ // writes no file (decided with the row, as `yad toolbox add` is) — and it never fails the command it is
641
+ // part of. Returns the part of the --json answer it adds: `{ used, findings, problems }`.
642
+ // `canRemove: false` leaves out the `remove` advice — for `yad join`, whose reader just cloned the team's
643
+ // choices: one person's machine lacking a tool is no reason to change what the whole team uses.
644
+ export function offerToolbox(root, productRoot, { canRemove = true, ...opts } = {}) {
645
+ let res;
646
+ // A step inside another command: if finding tools throws (a home folder that cannot be read), the
647
+ // command it is part of still finishes, and says why the step is missing.
648
+ try { res = toolboxCheck(root, productRoot, opts); } catch (e) {
649
+ warn(`toolbox: could not check the tools here (${clean(e?.message ?? String(e))}) — run \`yad toolbox check\` to see why`);
650
+ return { used: [], findings: [], problems: [], error: 'could not check' };
651
+ }
652
+ for (const p of res.problems) warn(p);
653
+ for (const line of toolboxCheckLines(res)) log(line);
654
+ if (res.findings.length) {
655
+ info(canRemove
656
+ ? 'nothing is installed for you: follow the steps you choose, or, for the whole team, stop using a tool with `yad toolbox remove <id>`'
657
+ : 'nothing is installed for you: follow the steps you choose');
658
+ }
659
+ return { used: res.used.map((r) => r.id), findings: res.findings, problems: res.problems };
660
+ }
661
+
631
662
  // `--detect skill:x,plugin:p` → { skills: ['x'], plugins: ['p'] }, or { error }.
632
663
  export function parseDetect(text) {
633
664
  const out = {};
package/cli/workspace.mjs CHANGED
@@ -32,6 +32,7 @@ import { productConfigPath, PROJECT_FILES } from './manifest.mjs';
32
32
  import { runSetup, insideWorkspace, throughGitDir, selectIdeTargets } from './setup.mjs';
33
33
  import { moduleActions, gitHookActions } from './plan.mjs';
34
34
  import { writeWorkspaceFile, WORKSPACE_FILE } from './find-product.mjs';
35
+ import { offerToolbox } from './toolbox.mjs';
35
36
 
36
37
  // E80: the file that lets yad find the Product from inside the repos it registers. Written by all three.
37
38
  function noteWorkspaceFile(product) {
@@ -483,13 +484,20 @@ export async function runJoin(cwd, url, folder, opts = {}) {
483
484
  const hook = gitHookActions(product);
484
485
  for (const a of hook) a.apply();
485
486
  if (hook.length) ok('installed the git pre-commit hook in this clone');
487
+ // E85: the tools the team chose (the Product's toolbox.json, just cloned) that this machine lacks. A
488
+ // new teammate is the person most likely to miss one. Offered, never installed.
489
+ log('');
490
+ log(c.bold('Toolbox'));
491
+ const toolbox = offerToolbox(product, product, { canRemove: false });
486
492
 
493
+ // Under a heading of its own, so these lines do not read as part of the toolbox list above.
487
494
  log('');
495
+ log(c.bold('Next'));
488
496
  if (repos.failed.length) hand(`${repos.failed.length} repo(s) not cloned — fix what is named above and re-run \`yad join ${shown(url)}\`; it keeps what is already there`);
489
497
  hand(`start with: cd ${fromShell(product)} && yad next`);
490
498
  return {
491
499
  workspace, product,
492
500
  repos, skills: { installed: skills.installed, stale: skills.stale.length, shared: skills.shared.length },
493
- hook: hook.length ? 'installed' : 'unchanged',
501
+ hook: hook.length ? 'installed' : 'unchanged', toolbox,
494
502
  };
495
503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yadflow",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "description": "Shared project memory and workflows for teams building with AI: a versioned record of plans, decisions, contracts and reviews across repos, human-approved review gates, and a zero-dependency `yad` CLI plus 38 skills for your AI assistant.",
5
5
  "type": "module",
6
6
  "author": "AbdelRahman Nasr",