yadflow 4.4.0 → 4.5.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,27 @@
1
+ # [4.5.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.4.0...v4.5.0) (2026-10-06)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **member:** E131 review 1 — links, twins, proof and pushes ([1179a97](https://github.com/abdelrahmannasr/yadflow/commit/1179a9731e4ff9f8d6b80f5aed82d5809bde5ed2))
7
+ * **member:** E131 review 2 — gate-live trust, account ids, clean-ups ([74d41f6](https://github.com/abdelrahmannasr/yadflow/commit/74d41f65a585fdf06639be87688f69353eb61916))
8
+ * **member:** E131 review 3 — trust only files the gate judged ([2f9af14](https://github.com/abdelrahmannasr/yadflow/commit/2f9af141e524ca7daacbce8c2799f56c72c6d75e))
9
+ * **member:** E131 review 4 — trust is read from origin's default branch ([43af9c4](https://github.com/abdelrahmannasr/yadflow/commit/43af9c4565c3bbfae7042d7765df62150fa64447))
10
+ * **member:** E131 review 5 — every change re-proves every email ([041937c](https://github.com/abdelrahmannasr/yadflow/commit/041937c936f5d9f29683ef6f9d4cc6fb93ad83bd))
11
+ * **member:** E131 review 6 — re-runs, origin/HEAD, partial lists ([e9bad00](https://github.com/abdelrahmannasr/yadflow/commit/e9bad002eff766705d193b1515baa3d39ae0c360))
12
+ * **standup:** E132 review 1 — gate rule, malformed ledgers, terminal safety ([a0e7d47](https://github.com/abdelrahmannasr/yadflow/commit/a0e7d4750e7b485582cded4b55dfcc212013a101))
13
+ * **standup:** E132 review 2 — missed twins of round 1 ([ba1ce61](https://github.com/abdelrahmannasr/yadflow/commit/ba1ce61b961decbea46298b714fd33f811fb264a))
14
+ * **standup:** E132 review 3 — paths, disputed accounts, left members ([add86cc](https://github.com/abdelrahmannasr/yadflow/commit/add86cca821ab29afa79838734a3b83621dd2330))
15
+ * **standup:** E132 review 4 — Windows paths, a name is plain text ([435225f](https://github.com/abdelrahmannasr/yadflow/commit/435225f48a21fe8a850ba65c0c7f1315df6bbb1e))
16
+ * **standup:** E132 review 5 — path names in square brackets ([7ac3cb2](https://github.com/abdelrahmannasr/yadflow/commit/7ac3cb265c309f0e707e6dae32076796a5e2c240))
17
+
18
+
19
+ ### Features
20
+
21
+ * **join:** E131 join ends with yad member add ([3295429](https://github.com/abdelrahmannasr/yadflow/commit/3295429092b549707a03eecacddc50d15d1df77a))
22
+ * **member:** E131 team member files, proven by the platform ([3e60bb6](https://github.com/abdelrahmannasr/yadflow/commit/3e60bb62bb2edb467be4d5f8562b9fe1bac6ca0a))
23
+ * **standup:** yad standup — a daily status per team member (E132) ([00f32ce](https://github.com/abdelrahmannasr/yadflow/commit/00f32ce742166e8b82e796b3eef85b379edd4cee))
24
+
1
25
  # [4.4.0](https://github.com/abdelrahmannasr/yadflow/compare/v4.3.0...v4.4.0) (2026-10-03)
2
26
 
3
27
 
package/README.md CHANGED
@@ -144,7 +144,7 @@ unless CI runs it), or `yad gate advance` with no platform, is the human decisio
144
144
  | --- | --- | --- |
145
145
  | Existing repositories | `yad init`, in the folder that holds them | Run the `yad-backfill` skill first. It records what already exists. |
146
146
  | One existing repository with all the code | `yad setup --brownfield --monorepo`, inside it | The same: run `yad-backfill` first. |
147
- | A team that already uses Yadflow | `yad join <Product clone URL>` | It clones the Product and every repository it lists. It sets up only this machine and never commits. |
147
+ | A team that already uses Yadflow | `yad join <Product clone URL>` | It clones the Product and every repository it lists and sets up this machine. Last, it records you as a team member (`yad member add`): your accounts and verified emails, on a branch of its own with its own pull request. |
148
148
 
149
149
  **Small changes.** Not every change needs the full route:
150
150
 
@@ -256,6 +256,7 @@ The record is worth trusting because people approve what goes into it.
256
256
  - the Shape and Build steps with review gates
257
257
  - linked changes and `yad thread`
258
258
  - `yad history`, `yad next` and `yad usage`
259
+ - `yad standup`, a daily status for each team member — what they did since the last working day, what they are on now, and what is waiting — rebuilt live from git, the ledgers and the platform, facts only
259
260
  - `yad detect`, which lists the skills, agents, MCP servers and plugins installed for each AI agent
260
261
  - `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); each skill that uses a tool reads the same answer and says what it does without it
261
262
  - `yad skill recommend`, which lists hand-picked skills from the recommended skill collections for each step, why each fits, and the command to bind one; it binds nothing, and every pick was checked against its source
package/bin/commands.mjs CHANGED
@@ -29,6 +29,8 @@ export { runCapture } from '../cli/capture.mjs';
29
29
  export { runFold } from '../cli/fold.mjs';
30
30
  export { runClaims } from '../cli/claims.mjs';
31
31
  export { runAssign, runUnassign, runOwners } from '../cli/owners.mjs';
32
+ export { runMemberAdd, runMemberList, runMemberRemove } from '../cli/members.mjs';
33
+ export { runStandup, foreignFlags as standupForeignFlags } from '../cli/standup.mjs';
32
34
  export { runIndex } from '../cli/index-command.mjs';
33
35
  export { runHistory, HISTORY_FLAGS } from '../cli/history.mjs';
34
36
  export { runRepo } from '../cli/repo.mjs';
package/bin/yad.mjs CHANGED
@@ -21,7 +21,8 @@ ${c.bold('Start a workspace')} ${c.dim('(the folder holding product/ and the cod
21
21
  Clone the Product into [folder]/product/ (default: the URL's last name) and
22
22
  every repo it registers; a failed repo clone is reported, never fatal. Then
23
23
  only per-machine steps: git-ignored skill copies + the git pre-commit hook.
24
- Never commits, pushes, or changes a shared file
24
+ Last, \`yad member add\` (below): your member file, on a branch with its own
25
+ PR/MR — the only shared change join makes. A problem there never fails join
25
26
 
26
27
  ${c.bold('Setup & maintenance')}
27
28
  yad setup Guided first-run setup (profile interview, install, connect & wire repos,
@@ -68,6 +69,31 @@ ${c.bold('Setup & maintenance')}
68
69
  installs in this clone refuses a person's hand commit to the ledger.
69
70
  YAD_HOOK_DISABLE=1 skips.
70
71
 
72
+ ${c.bold('Team members')} ${c.dim('(identity only — no roles, no reviewers, no allowlist)')}
73
+ yad member add [--no-push] Prove who you are and write .sdlc/members/<platform>-<login>.json:
74
+ your account on the Product's platform (logs you in if needed),
75
+ your other GitHub/GitLab accounts and git emails that the platform
76
+ says are VERIFIED (stored hashed). One commit per email on
77
+ yad/member/<login>, pushed, with a PR/MR. Never your checkout
78
+ yad member list [--json] Every member: active (committed or approved within the TTL,
79
+ default 90 days), idle (quiet, still has access), left (the
80
+ platform says no access — off the team list) or unknown
81
+ yad member remove [<login>] [--reason <why>]
82
+ Delete a member file — your own, or anyone's with --reason
83
+
84
+ ${c.bold('Daily standup')} ${c.dim('(derived live, read-only, facts only — no score, no ranking)')}
85
+ yad standup Each member on the team list: done since the last standup
86
+ (commits, approvals, comments, closed steps, ships, merged
87
+ PRs/MRs), working on now (owned steps, open claims, drafts, open
88
+ PRs/MRs) and waiting (gates on their steps, their next move, their
89
+ PRs/MRs with failing checks or no approving review). Then the
90
+ people seen who are not on the list, and who has left.
91
+ Window: from midnight of the previous working day (Monday reads
92
+ back to Friday). Asks the platform about the Product and every
93
+ registered repo; one it cannot read says "platform not read"
94
+ flags: --since <YYYY-MM-DD>|<N>h|<N>d, --tz <zone> (default UTC),
95
+ --member <login> | --me, --format md|html, --out <file>, --json
96
+
71
97
  ${c.bold('Team usage (EM adoption & behavior report)')}
72
98
  yad usage Build a per-member report (HTML) from git + the SDLC ledgers
73
99
  (derived, read-only — writes no tracked state)
@@ -349,7 +375,9 @@ ${c.bold('Environment')}
349
375
  YAD_NO_REPORT=1 Never offer to file a bug report after a failure
350
376
  YAD_PLATFORM_LOGIN=0 Name a record's author by git user.name; never ask gh/glab who is logged in`;
351
377
 
352
- 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']);
378
+ const STANDUP_USAGE = 'usage: yad standup [--since <YYYY-MM-DD>|<N>h|<N>d] [--tz <zone>] [--member <login>|--me] [--format md|html] [--out <file>] [--json]';
379
+
380
+ 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', '--tz']);
353
381
 
354
382
  function parseArgs(argv) {
355
383
  const o = { _: [], dir: process.cwd(), fix: false, force: false, scope: 'all' };
@@ -360,6 +388,7 @@ function parseArgs(argv) {
360
388
  else if (a === '--force') o.force = true;
361
389
  else if (a === '--contract-change') o.contractChange = true;
362
390
  else if (a === '--manual') o.manual = true;
391
+ else if (a === '--me') o.me = true;
363
392
  else if (a === '--no-push') o.noPush = true;
364
393
  else if (a === '--no-fetch') o.noFetch = true;
365
394
  else if (a === '--push') o.push = true;
@@ -447,6 +476,7 @@ const ACTIONS = {
447
476
  reconcile: { known: ['check', 'refresh', 'wire'] },
448
477
  hook: { known: ['ledger-guard'] },
449
478
  toolbox: { known: ['list', 'check', 'add', 'remove'], default: 'list' },
479
+ member: { known: ['add', 'list', 'remove'], default: 'list' },
450
480
  };
451
481
  // E80 — how each command finds its folder. A PRODUCT command works on the Product; a REPO command works
452
482
  // on the code repo it runs in and reads the Product beside it. The rest take their folder as given:
@@ -455,7 +485,7 @@ const PRODUCT_CMDS = new Set([
455
485
  'check', 'update', 'doctor', 'migrate', 'usage', 'sync-status', 'epic', 'foundation', 'skill', 'next',
456
486
  'skip', 'unskip', 'defer', 'undefer', 'dial', 'mode', 'kill', 'unkill', 'unblock', 'gate', 'checkpoint',
457
487
  'capture', 'claims', 'assign', 'unassign', 'owners', 'fold', 'tidy', 'index', 'history', 'repo',
458
- 'risk-map', 'codeowners', 'docs', 'thread', 'reconcile',
488
+ 'risk-map', 'codeowners', 'docs', 'thread', 'reconcile', 'member', 'standup',
459
489
  ]);
460
490
  const REPO_CMDS = new Set(['commit', 'open-pr', 'ship', 'review']);
461
491
  // The Product commands that WRITE its files without first checking it is one. Run from a code repo they
@@ -468,6 +498,7 @@ function writesProduct(cmd, o) {
468
498
  if (cmd === 'epic' || cmd === 'foundation') return action === 'new';
469
499
  if (cmd === 'skill') return action === 'bind' || action === 'unbind';
470
500
  if (cmd === 'dial') return o.to !== undefined;
501
+ if (cmd === 'member') return o._[1] === 'add' || o._[1] === 'remove';
471
502
  // `--wire` writes a CI workflow; `--refresh` only rebuilds sites the folder already holds.
472
503
  if (cmd === 'docs') return action === 'sync' && !!o.wire;
473
504
  return false;
@@ -887,6 +918,30 @@ async function main() {
887
918
  if (o.hook) process.exitCode = 0;
888
919
  break;
889
920
  }
921
+ case 'member': {
922
+ // E131. One file per person, identity only.
923
+ const [, action = 'list', who, extra] = o._;
924
+ const usage = 'usage: yad member add [--no-push] | yad member list [--json] | yad member remove [<login>] [--reason "<why>"]';
925
+ if (!['add', 'list', 'remove'].includes(action)) { refuse(`unknown member action: ${action} (add, list, remove)`, usage); break; }
926
+ if ((action !== 'remove' && who) || extra) { refuse(`unexpected word: ${extra || who}`, usage); break; }
927
+ if (noProduct()) break;
928
+ if (action === 'add') result = await commands.runMemberAdd(o.dir, { noPush: !!o.noPush, today });
929
+ else if (action === 'list') result = await commands.runMemberList(o.dir, { today });
930
+ else result = await commands.runMemberRemove(o.dir, { login: who ?? null, reason: o.reason ?? null });
931
+ break;
932
+ }
933
+ case 'standup': {
934
+ // E132. Read-only; every flag that is not standup's own is refused, as history refuses them.
935
+ const foreign = commands.standupForeignFlags(process.argv.slice(2));
936
+ if (foreign.length) { refuse(`yad standup does not take ${foreign.join(', ')}`, STANDUP_USAGE); break; }
937
+ if (o._[1]) { refuse(`yad standup takes no words (got: ${o._[1]})`, STANDUP_USAGE); break; }
938
+ if (noProduct()) break;
939
+ result = await commands.runStandup(o.dir, {
940
+ since: o.since ?? null, tz: o.tz ?? 'UTC', member: o.member ?? null, me: !!o.me,
941
+ format: o.format ?? null, out: o.out ?? null, json: !!o.json,
942
+ });
943
+ break;
944
+ }
890
945
  case 'claims': {
891
946
  // E46. Read from the capture branches; nothing is recorded here.
892
947
  const [, epic, extra] = o._;
package/cli/claims.mjs CHANGED
@@ -135,6 +135,21 @@ export function claimWarnings(root, changed, { env = process.env, now = Date.now
135
135
  }
136
136
  export const warningText = (hits, now) => `yad claims: ${hits.length === 1 ? 'this file is' : 'these files are'} also being edited by someone else — advice, not a lock; talk to them before you go further:\n${hits.map((c) => ` • ${claimLine(c, now)}`).join('\n')}`;
137
137
 
138
+ // Fetch everyone's capture branches and the default branch: 'done' | 'failed' | 'local' (no origin). One
139
+ // fetch, 30 seconds at most. `yad claims` and `yad standup` (E132) both read what it brings.
140
+ export function fetchCaptures(root, env = process.env) {
141
+ const git = gitIn(root, env);
142
+ if (!git(['remote', 'get-url', 'origin']).ok) return 'local';
143
+ const opts = { cwd: root, encoding: 'utf8', timeout: 30_000, env: { ...env, ...pushEnv(env) } };
144
+ const r = spawnSync('git', fetchAllArgs({ prune: true }), opts);
145
+ const fetched = r.status === 0 ? 'done' : 'failed';
146
+ // The default branch, for the "landed" rule — on its own: a name guessed wrong (no Product default, no
147
+ // origin/HEAD) must not stop the capture branches from arriving. Its failure changes nothing.
148
+ const def = defaultRef(root, git).name;
149
+ if (fetched === 'done') spawnSync('git', ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=20', 'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${def}:refs/remotes/origin/${def}`], opts);
150
+ return fetched;
151
+ }
152
+
138
153
  // `yad claims [<epic>] [--no-fetch]`. Returns a plain object (E1 makes it the `--json` answer).
139
154
  export async function runClaims(root, { epic = null, noFetch = false, env = process.env, now = Date.now() } = {}) {
140
155
  const refuse = (msg, hint) => { fail(msg); if (hint) hand(hint); process.exitCode = 1; };
@@ -142,19 +157,7 @@ export async function runClaims(root, { epic = null, noFetch = false, env = proc
142
157
  if (!git(['rev-parse', '--show-toplevel']).ok) return refuse('not a git repository — there are no capture branches to read');
143
158
  if (!fs.existsSync(productConfigPath(root))) return refuse('not a Product (no .sdlc/hub.json or product.json here)', 'run it from the Product root, or pass --dir');
144
159
  if (epic && !/^EP-[a-z0-9-]+$/.test(epic)) return refuse(`not an epic id: ${epic}`);
145
- let fetched = 'skipped';
146
- if (!noFetch) {
147
- if (!git(['remote', 'get-url', 'origin']).ok) fetched = 'local';
148
- else {
149
- const opts = { cwd: root, encoding: 'utf8', timeout: 30_000, env: { ...env, ...pushEnv(env) } };
150
- const r = spawnSync('git', fetchAllArgs({ prune: true }), opts);
151
- fetched = r.status === 0 ? 'done' : 'failed';
152
- // The default branch, for the "landed" rule — on its own: a name guessed wrong (no Product default, no
153
- // origin/HEAD) must not stop the capture branches from arriving. Its failure changes nothing.
154
- const def = defaultRef(root, git).name;
155
- if (fetched === 'done') spawnSync('git', ['-c', 'http.lowSpeedLimit=1000', '-c', 'http.lowSpeedTime=20', 'fetch', '--quiet', '--no-tags', 'origin', `+refs/heads/${def}:refs/remotes/origin/${def}`], opts);
156
- }
157
- }
160
+ const fetched = noFetch ? 'skipped' : fetchCaptures(root, env);
158
161
  if (fetched === 'local') info('no remote named origin — only your own captures can be read here');
159
162
  if (fetched === 'failed') warn('could not fetch the capture branches — showing what was last fetched, which may be old');
160
163
  const { claims, defaultBranch } = readClaims(root, { env, now, epics: epic ? new Set([epic]) : null });
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 { readMembers, judgedMemberFiles } from './members.mjs';
28
29
  import { loadChoices, readToolboxFile } from './toolbox.mjs';
29
30
 
30
31
  // A registered path doctor may run git in (E81): a checkout the judgement accepts — never a refused
@@ -1889,6 +1890,37 @@ export function ownerChecks(checks, root) {
1889
1890
  'such a file is ignored — no owner is shown and the edit-time warning is off. For a step on the chain, `yad assign <epic> <step> --force` replaces it and `yad unassign <epic> <step> --force` removes it; for any other, delete it (`git rm <path>`). `yad owners` lists them all');
1890
1891
  }
1891
1892
 
1893
+ // `members:*` (E131). A member file that cannot be used, or an account or email two files share, pairs
1894
+ // nothing — the person it was meant to join counts twice, and nobody is told anywhere else. And where no
1895
+ // CI runs (a local ledger, no platform), nothing checks a member file: say that it is its owner's statement.
1896
+ export function memberChecks(checks, root) {
1897
+ let got;
1898
+ try { got = readMembers(root); } catch { return; }
1899
+ const some = (list) => `${list.slice(0, 3).join('; ')}${list.length > 3 ? ` (+${list.length - 3} more)` : ''}`;
1900
+ if (got.errors.length) {
1901
+ check(checks, 'members:unreadable', 'project', 'warn', `${got.errors.length} member file(s) pair nothing: ${some(got.errors.map((e) => e.error))}`,
1902
+ 'such a file joins no email to any login, so its person counts twice and is not on the team list. Its owner runs `yad member add` to write it again; otherwise delete it (`yad member remove <login> --reason …`)');
1903
+ }
1904
+ if (got.duplicates.length) {
1905
+ check(checks, 'members:duplicate', 'project', 'warn', `an account or email is in two member files: ${some(got.duplicates)}`,
1906
+ 'one account and one email belong to one member. Until one file is fixed, neither file joins it. On a verified Product the member-check gate refuses the PR that adds the second');
1907
+ }
1908
+ // Which files the gate COUNT leaves out, and why (review 3): no live gate, or a file the gate never judged.
1909
+ if (got.members.length && got.identity.verified && got.identity.platform === 'github') {
1910
+ const judged = judgedMemberFiles(root, got.identity);
1911
+ const left = got.members.filter((m) => !judged.files.has(m.rel)).map((m) => m.rel);
1912
+ if (left.length) {
1913
+ check(checks, 'members:untrusted', 'project', 'warn', `${left.length} member file(s) are not used by the active-people count: ${left.slice(0, 3).join('; ')}${left.length > 3 ? ` (+${left.length - 3} more)` : ''}`,
1914
+ judged.live ? 'no gate as shipped judged them: each was last changed before the member-check gate or its workflow last changed on origin\'s default branch (every `yad update` that changes either resets this), or differs here from origin — its owner runs `yad member add` again, and that change is judged'
1915
+ : 'the member-check gate or its workflow on origin\'s default branch is missing, edited, or from another yadflow version than this one — `yad update` installs the shipped copies, then each member runs `yad member add` again');
1916
+ }
1917
+ }
1918
+ if (got.members.length && !got.identity.verified) {
1919
+ check(checks, 'members:unchecked', 'project', 'ok', `${got.members.length} member file(s), and no CI checks them on this Product`,
1920
+ 'with a local ledger or no platform, no member-check gate runs, so each member file is its owner\'s own statement');
1921
+ }
1922
+ }
1923
+
1892
1924
  // The installed gates whose changed list must name a rename by both paths (E114), and the test for one
1893
1925
  // that does not. Read as bash reads it, one command at a time, so one fixed command never hides its blind
1894
1926
  // twin: a `#` line is prose and is dropped FIRST (a `\` at its end does not go on); then a line ending in
@@ -2563,6 +2595,7 @@ export function collectDoctor(root, { headCount = null } = {}) {
2563
2595
  skipChecks(checks, root);
2564
2596
  stepStateChecks(checks, root);
2565
2597
  ownerChecks(checks, root);
2598
+ memberChecks(checks, root);
2566
2599
  ownerGuardChecks(checks, root);
2567
2600
  renamedChecks(checks, root);
2568
2601
  productProfileChecks(checks, root);
package/cli/manifest.mjs CHANGED
@@ -539,6 +539,9 @@ export const PRODUCT_WIRING = {
539
539
  { src: 'skills/yad-checks/templates/checks/commit-message.sh', dest: 'checks/commit-message.sh', exec: true },
540
540
  { src: 'skills/yad-pr-template/templates/checks/pr-title.sh', dest: 'checks/pr-title.sh', exec: true },
541
541
  { src: 'skills/yad-pr-template/templates/checks/pr-template.sh', dest: 'checks/pr-template.sh', exec: true },
542
+ // E131: a member file is changed only by its own person, and each email it adds is proven on the PR.
543
+ // A Node script (`node checks/member-check.mjs`), so it needs no execute bit.
544
+ { src: 'skills/yad-checks/templates/checks/member-check.mjs', dest: 'checks/member-check.mjs' },
542
545
  ],
543
546
  github: [
544
547
  { src: 'skills/yad-product-bridge/templates/github/yad-gate-sync.yml', dest: '.github/workflows/yad-gate-sync.yml' },