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 +24 -0
- package/README.md +2 -1
- package/bin/commands.mjs +2 -0
- package/bin/yad.mjs +58 -3
- package/cli/claims.mjs +16 -13
- package/cli/doctor.mjs +33 -0
- package/cli/manifest.mjs +3 -0
- package/cli/members.mjs +822 -0
- package/cli/people.mjs +13 -3
- package/cli/platform.mjs +1 -1
- package/cli/pr-list.mjs +125 -0
- package/cli/protection.mjs +4 -4
- package/cli/standup.mjs +709 -0
- package/cli/usage.mjs +18 -11
- package/cli/workspace.mjs +14 -5
- package/package.json +5 -3
- package/skills/yad-checks/templates/checks/member-check.mjs +197 -0
- package/skills/yad-checks/templates/github/yad-product-checks.yml +15 -0
- package/skills/yad-checks/templates/gitlab/yad-product-checks.gitlab-ci.yml +9 -0
- package/skills/yad-status/SKILL.md +3 -0
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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' },
|