yadflow 4.3.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 +63 -0
- package/README.md +4 -2
- package/bin/commands.mjs +3 -1
- package/bin/yad.mjs +80 -14
- package/cli/claims.mjs +16 -13
- package/cli/detect.mjs +2 -2
- package/cli/docs.mjs +1 -1
- package/cli/doctor.mjs +108 -3
- package/cli/epic-state.mjs +121 -24
- package/cli/epic.mjs +7 -5
- package/cli/manifest.mjs +3 -0
- package/cli/members.mjs +822 -0
- package/cli/next.mjs +21 -9
- 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/recommend.mjs +301 -0
- package/cli/setup.mjs +3 -2
- package/cli/skill.mjs +263 -63
- package/cli/standup.mjs +709 -0
- package/cli/toolbox.mjs +433 -11
- package/cli/usage.mjs +18 -11
- package/cli/workspace.mjs +14 -5
- package/package.json +5 -3
- package/skills/sdlc/config.yaml +1 -1
- package/skills/sdlc/module-help.csv +1 -1
- package/skills/yad-backfill/SKILL.md +40 -7
- package/skills/yad-backfill/references/backfill.md +5 -4
- package/skills/yad-change/SKILL.md +1 -1
- 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-connect-design/SKILL.md +16 -0
- package/skills/yad-connect-learning/SKILL.md +14 -0
- package/skills/yad-connect-repos/SKILL.md +34 -5
- package/skills/yad-connect-repos/references/code-context.md +5 -5
- package/skills/yad-connect-testing/SKILL.md +20 -0
- package/skills/yad-docs/SKILL.md +1 -1
- package/skills/yad-epic/references/state-schema.md +10 -2
- package/skills/yad-learn/SKILL.md +14 -0
- package/skills/yad-run/SKILL.md +7 -4
- package/skills/yad-run/references/run-loop.md +2 -1
- package/skills/yad-spec/SKILL.md +37 -6
- package/skills/yad-spec/references/spec-handoff.md +2 -2
- package/skills/yad-status/SKILL.md +3 -0
- package/skills/yad-test-cases/SKILL.md +20 -0
- package/skills/yad-ui/SKILL.md +36 -3
package/cli/toolbox.mjs
CHANGED
|
@@ -46,33 +46,75 @@ const TIER_TITLES = { core: 'Core — offered at setup', recommended: 'Recommend
|
|
|
46
46
|
|
|
47
47
|
const CHECKED = '2026-10-01';
|
|
48
48
|
|
|
49
|
+
// ---- vetting (E88) --------------------------------------------------------------------------------
|
|
50
|
+
//
|
|
51
|
+
// EVERY ENTRY IS VETTED BEFORE IT SHIPS, and the vetting is redone before it goes stale. `checked` above
|
|
52
|
+
// is when an entry's INSTALL routes were read; `vetted` is the proof that the tool is what it claims:
|
|
53
|
+
//
|
|
54
|
+
// on the day the vetting was done. Per entry: they all start on VETTED_ON, and re-vetting ONE
|
|
55
|
+
// tool gives that entry its own date literal rather than moving the shared one
|
|
56
|
+
// licenceFrom where the licence was read — the repository's LICENSE file, or, for a closed tool, the
|
|
57
|
+
// vendor's own terms page. `licence` on the entry is the answer; this is the proof
|
|
58
|
+
// source the official home the tool comes from (its owner's repository or docs)
|
|
59
|
+
// release the source's latest release tag on that day, or null where none is published. A tag is
|
|
60
|
+
// provenance, not a version: tags read `v1.18.1`, `skill-v4.5.0`, `cli-2.11.0`
|
|
61
|
+
// packages each registry package an install route names, confirmed to point back to `source`
|
|
62
|
+
// (npm `repository`, or the source's own README naming it), with the version on that day
|
|
63
|
+
// note anything a reader of the record has to know — a missing LICENSE, a rebrand
|
|
64
|
+
//
|
|
65
|
+
// Licence and source only, as decided with the row: no maintenance score, no security scan, and no
|
|
66
|
+
// tested version range — `versions` stays null. A team's own tools (`yad toolbox add --custom`) are the
|
|
67
|
+
// team's to vet; yad vets only the list it ships.
|
|
68
|
+
//
|
|
69
|
+
// TWO GATES. The unit suite holds every record complete (`vettingProblems`); the release check refuses
|
|
70
|
+
// to publish while any record is older than VET_MAX_AGE_DAYS (`staleVettings`, `scripts/vet-check.mjs`).
|
|
71
|
+
// The age is checked only at release, never in the unit suite, so `main` does not turn red by itself.
|
|
72
|
+
const VETTED_ON = '2026-10-02';
|
|
73
|
+
export const VET_MAX_AGE_DAYS = 180;
|
|
74
|
+
const VET_REGISTRIES = Object.freeze(['npm', 'pypi']);
|
|
75
|
+
|
|
49
76
|
// The list itself, checked against each tool's own repository and docs on CHECKED. `install` keeps only
|
|
50
77
|
// the routes the roadmap's four types describe (plugin, npm, python, script); a tool's other routes
|
|
51
78
|
// (Docker, Homebrew, a VS Code extension) are in its own README, which `source` links. `versions` is null
|
|
52
|
-
// everywhere
|
|
53
|
-
//
|
|
54
|
-
//
|
|
79
|
+
// everywhere: no bound has been tested, and E88 vets licence and source, not a version range. `records`
|
|
80
|
+
// is the exact line a skill writes when the tool is absent. Each skill in
|
|
81
|
+
// `usedBy` quotes `fallback` and `records` word for word in its "When a tool is missing" section (E87):
|
|
82
|
+
// change the words here, then rewrite the sections with `skillFallbackSection`.
|
|
55
83
|
export const TOOLBOX = Object.freeze([
|
|
56
84
|
// ---- core: offered at setup --------------------------------------------------------------------
|
|
57
85
|
{
|
|
58
86
|
id: 'repomix', name: 'Repomix', tier: 'core', role: 'packs each code repo into one file the Shape steps read',
|
|
59
87
|
licence: 'MIT', source: 'https://github.com/yamadashy/repomix', checked: CHECKED,
|
|
88
|
+
vetted: {
|
|
89
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/yamadashy/repomix/blob/main/LICENSE',
|
|
90
|
+
source: 'https://github.com/yamadashy/repomix', release: 'v1.18.1',
|
|
91
|
+
packages: [{ registry: 'npm', name: 'repomix', version: '1.18.1' }],
|
|
92
|
+
note: null,
|
|
93
|
+
},
|
|
60
94
|
install: [
|
|
61
|
-
{ type: 'npm', command: 'npx repomix@
|
|
62
|
-
{ type: 'npm', command: 'npm install -g repomix' },
|
|
95
|
+
{ type: 'npm', command: 'npx repomix@1.18.1' },
|
|
96
|
+
{ type: 'npm', command: 'npm install -g repomix@1.18.1' },
|
|
63
97
|
{ type: 'plugin', command: '/plugin marketplace add yamadashy/repomix, then /plugin install repomix-mcp@repomix' },
|
|
64
98
|
],
|
|
65
99
|
manual: null,
|
|
66
|
-
// yad runs it through
|
|
100
|
+
// yad runs it through npx, at the vetted version (setup, `yad repo refresh`; REPOMIX_VERSION below),
|
|
101
|
+
// so `npx` alone makes it usable.
|
|
67
102
|
detect: { npx: true, bins: ['repomix'], plugins: ['repomix-mcp', 'repomix-commands', 'repomix-explorer'], mcp: ['repomix'], skills: ['repomix-explorer'] },
|
|
68
103
|
versions: null,
|
|
69
|
-
fallback: 'no
|
|
70
|
-
records:
|
|
71
|
-
|
|
104
|
+
fallback: 'no Repomix pack. yad-connect-repos and yad-backfill put the same context together by hand, from the source tree and the recent git log, and the Shape steps read the code map',
|
|
105
|
+
records: 'source: repomix-unavailable',
|
|
106
|
+
note: 'yad setup and yad repo refresh skip the pack only when npx is missing. They do not follow a team skip.',
|
|
107
|
+
usedBy: ['yad setup', 'yad repo refresh', 'yad-connect-repos', 'yad-backfill'],
|
|
72
108
|
},
|
|
73
109
|
{
|
|
74
110
|
id: 'spec-kit', name: 'Spec Kit', tier: 'core', role: 'runs the spec ceremony (specify → plan → tasks) in a code repo',
|
|
75
111
|
licence: 'MIT', source: 'https://github.com/github/spec-kit', checked: CHECKED,
|
|
112
|
+
vetted: {
|
|
113
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/github/spec-kit/blob/main/LICENSE',
|
|
114
|
+
source: 'https://github.com/github/spec-kit', release: 'v1.0.13',
|
|
115
|
+
packages: [{ registry: 'pypi', name: 'specify-cli', version: '1.0.13' }],
|
|
116
|
+
note: "PyPI's specify-cli lists no project links and no provenance; Spec Kit's own README names `uv tool install specify-cli`, which is the link to the source.",
|
|
117
|
+
},
|
|
76
118
|
install: [
|
|
77
119
|
{ type: 'python', command: 'uv tool install specify-cli, then specify init <project> --integration claude' },
|
|
78
120
|
{ type: 'python', command: 'pipx install specify-cli' },
|
|
@@ -89,6 +131,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
89
131
|
{
|
|
90
132
|
id: 'impeccable', name: 'Impeccable', tier: 'core', role: 'drives the UI design step (document, extract, craft)',
|
|
91
133
|
licence: 'Apache-2.0', source: 'https://github.com/pbakaus/impeccable', checked: CHECKED,
|
|
134
|
+
vetted: {
|
|
135
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/pbakaus/impeccable/blob/main/LICENSE',
|
|
136
|
+
source: 'https://github.com/pbakaus/impeccable', release: 'skill-v4.5.0',
|
|
137
|
+
packages: [{ registry: 'npm', name: 'impeccable', version: '4.1.0' }],
|
|
138
|
+
note: "The skill and the npm installer are versioned separately: the release tag is the skill, the npm version is the installer.",
|
|
139
|
+
},
|
|
92
140
|
install: [
|
|
93
141
|
{ type: 'npm', command: 'npx impeccable install' },
|
|
94
142
|
{ type: 'plugin', command: '/plugin marketplace add pbakaus/impeccable, then /plugin install impeccable@impeccable' },
|
|
@@ -96,7 +144,7 @@ export const TOOLBOX = Object.freeze([
|
|
|
96
144
|
manual: null,
|
|
97
145
|
detect: { skills: ['impeccable'], plugins: ['impeccable'] },
|
|
98
146
|
versions: null,
|
|
99
|
-
fallback: '
|
|
147
|
+
fallback: 'markdown-only UI design: yad-ui writes ui-design.md and DESIGN.md directly',
|
|
100
148
|
records: 'impeccable: not-installed', note: null,
|
|
101
149
|
usedBy: ['yad-ui'],
|
|
102
150
|
},
|
|
@@ -106,6 +154,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
106
154
|
id: 'bmad-method', name: 'BMAD-METHOD', tier: 'recommended', role: 'a pool of planning and building skills',
|
|
107
155
|
// The LICENSE file is MIT text with attribution added (GitHub reads it as NOASSERTION); "BMad" is a trademark.
|
|
108
156
|
licence: 'MIT', source: 'https://github.com/bmad-code-org/BMAD-METHOD', checked: CHECKED,
|
|
157
|
+
vetted: {
|
|
158
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/bmad-code-org/BMAD-METHOD/blob/main/LICENSE',
|
|
159
|
+
source: 'https://github.com/bmad-code-org/BMAD-METHOD', release: 'v6.12.0',
|
|
160
|
+
packages: [{ registry: 'npm', name: 'bmad-method', version: '6.12.0' }],
|
|
161
|
+
note: "GitHub reads the LICENSE as NOASSERTION: it is MIT text with an attribution clause added; npm declares MIT.",
|
|
162
|
+
},
|
|
109
163
|
install: [
|
|
110
164
|
{ type: 'npm', command: 'npx bmad-method install' },
|
|
111
165
|
{ type: 'plugin', command: '/plugin marketplace add bmad-code-org/bmad-plugins, then install bmad-method from /plugin' },
|
|
@@ -121,6 +175,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
121
175
|
{
|
|
122
176
|
id: 'ecc', name: 'ECC (Everything Claude Code)', tier: 'recommended', role: 'a large pool of skills, agents and hooks',
|
|
123
177
|
licence: 'MIT', source: 'https://github.com/affaan-m/ECC', checked: CHECKED,
|
|
178
|
+
vetted: {
|
|
179
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/affaan-m/ECC/blob/main/LICENSE',
|
|
180
|
+
source: 'https://github.com/affaan-m/ECC', release: 'v2.2.3',
|
|
181
|
+
packages: [{ registry: 'npm', name: 'ecc-universal', version: '2.2.3' }],
|
|
182
|
+
note: null,
|
|
183
|
+
},
|
|
124
184
|
install: [
|
|
125
185
|
{ type: 'npm', command: 'npx ecc-universal install --guided' },
|
|
126
186
|
{ type: 'plugin', command: '/plugin marketplace add https://github.com/affaan-m/ECC, then /plugin install ecc@ecc' },
|
|
@@ -137,6 +197,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
137
197
|
{
|
|
138
198
|
id: 'mattpocock-skills', name: 'mattpocock/skills', tier: 'recommended', role: 'a small pool of engineering skills',
|
|
139
199
|
licence: 'MIT', source: 'https://github.com/mattpocock/skills', checked: CHECKED,
|
|
200
|
+
vetted: {
|
|
201
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/mattpocock/skills/blob/main/LICENSE',
|
|
202
|
+
source: 'https://github.com/mattpocock/skills', release: 'v1.2.3',
|
|
203
|
+
packages: [],
|
|
204
|
+
note: null,
|
|
205
|
+
},
|
|
140
206
|
install: [{ type: 'plugin', command: 'claude plugins install mattpocock-skills' }],
|
|
141
207
|
manual: null,
|
|
142
208
|
// Generic names (`tdd`, `code-review`) collide with other packs; this one is its own.
|
|
@@ -151,6 +217,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
151
217
|
{
|
|
152
218
|
id: 'figma', name: 'Figma', tier: 'connector', role: 'design tool: yad-ui generates and links screens in it',
|
|
153
219
|
licence: 'proprietary', source: 'https://developers.figma.com/docs/figma-mcp-server/', checked: CHECKED,
|
|
220
|
+
vetted: {
|
|
221
|
+
on: VETTED_ON, licenceFrom: 'https://www.figma.com/legal/tos/',
|
|
222
|
+
source: 'https://github.com/figma/mcp-server-guide', release: null,
|
|
223
|
+
packages: [],
|
|
224
|
+
note: "The plugin comes from Anthropic's official marketplace, which points at figma/mcp-server-guide; that repository has no LICENSE file, so the proof of terms is Figma's own.",
|
|
225
|
+
},
|
|
154
226
|
install: [{ type: 'plugin', command: 'claude plugin install figma@claude-plugins-official' }],
|
|
155
227
|
manual: 'https://developers.figma.com/docs/figma-mcp-server/',
|
|
156
228
|
detect: { plugins: ['figma'], mcp: ['figma', 'figma-desktop', 'html-to-design'] },
|
|
@@ -162,6 +234,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
162
234
|
{
|
|
163
235
|
id: 'pencil', name: 'Pencil (pen.dev)', tier: 'connector', role: 'design tool: yad-ui writes .pen web and mobile screens',
|
|
164
236
|
licence: 'proprietary', source: 'https://docs.pencil.dev/getting-started/installation', checked: CHECKED,
|
|
237
|
+
vetted: {
|
|
238
|
+
on: VETTED_ON, licenceFrom: 'https://www.pen.dev/terms-of-use',
|
|
239
|
+
source: 'https://docs.pencil.dev/getting-started/installation', release: null,
|
|
240
|
+
packages: [],
|
|
241
|
+
note: "Pencil now publishes as pen.dev; its terms live there and the old docs address still answers.",
|
|
242
|
+
},
|
|
165
243
|
install: [],
|
|
166
244
|
manual: 'https://docs.pencil.dev/getting-started/installation',
|
|
167
245
|
detect: { mcp: ['pencil'] },
|
|
@@ -173,6 +251,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
173
251
|
{
|
|
174
252
|
id: 'playwright', name: 'Playwright MCP', tier: 'connector', role: 'testing tool: generates and runs browser and API tests',
|
|
175
253
|
licence: 'Apache-2.0', source: 'https://github.com/microsoft/playwright-mcp', checked: CHECKED,
|
|
254
|
+
vetted: {
|
|
255
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/microsoft/playwright-mcp/blob/main/LICENSE',
|
|
256
|
+
source: 'https://github.com/microsoft/playwright-mcp', release: 'v0.0.83',
|
|
257
|
+
packages: [{ registry: 'npm', name: '@playwright/mcp', version: '0.0.83' }],
|
|
258
|
+
note: null,
|
|
259
|
+
},
|
|
176
260
|
install: [{ type: 'npm', command: 'claude mcp add playwright npx @playwright/mcp@latest' }],
|
|
177
261
|
manual: null,
|
|
178
262
|
detect: { mcp: ['playwright'], plugins: ['playwright'] },
|
|
@@ -185,6 +269,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
185
269
|
{
|
|
186
270
|
id: 'cypress', name: 'Cypress', tier: 'connector', role: 'testing tool: Cypress specs',
|
|
187
271
|
licence: 'proprietary', source: 'https://docs.cypress.io/cloud/integrations/cloud-mcp', checked: CHECKED,
|
|
272
|
+
vetted: {
|
|
273
|
+
on: VETTED_ON, licenceFrom: 'https://www.cypress.io/terms-of-use',
|
|
274
|
+
source: 'https://docs.cypress.io/cloud/integrations/cloud-mcp', release: null,
|
|
275
|
+
packages: [],
|
|
276
|
+
note: null,
|
|
277
|
+
},
|
|
188
278
|
install: [],
|
|
189
279
|
manual: 'https://docs.cypress.io/cloud/integrations/cloud-mcp',
|
|
190
280
|
detect: { mcp: ['cypress', 'cypress-cloud'] },
|
|
@@ -197,6 +287,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
197
287
|
{
|
|
198
288
|
id: 'pytest', name: 'pytest', tier: 'connector', role: 'testing tool: service-layer tests',
|
|
199
289
|
licence: 'MIT', source: 'https://github.com/pytest-dev/pytest', checked: CHECKED,
|
|
290
|
+
vetted: {
|
|
291
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/pytest-dev/pytest/blob/main/LICENSE',
|
|
292
|
+
source: 'https://github.com/pytest-dev/pytest', release: '9.1.1',
|
|
293
|
+
packages: [],
|
|
294
|
+
note: null,
|
|
295
|
+
},
|
|
200
296
|
install: [],
|
|
201
297
|
manual: 'https://docs.pytest.org/',
|
|
202
298
|
detect: { mcp: ['pytest'] },
|
|
@@ -209,6 +305,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
209
305
|
{
|
|
210
306
|
id: 'maestro', name: 'Maestro', tier: 'connector', role: 'testing tool: mobile UI flows',
|
|
211
307
|
licence: 'Apache-2.0', source: 'https://github.com/mobile-dev-inc/Maestro', checked: CHECKED,
|
|
308
|
+
vetted: {
|
|
309
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/mobile-dev-inc/Maestro/blob/main/LICENSE',
|
|
310
|
+
source: 'https://github.com/mobile-dev-inc/Maestro', release: 'cli-2.11.0',
|
|
311
|
+
packages: [],
|
|
312
|
+
note: null,
|
|
313
|
+
},
|
|
212
314
|
install: [{ type: 'script', command: 'curl -fsSL "https://get.maestro.mobile.dev" | bash, then claude mcp add maestro -- maestro mcp' }],
|
|
213
315
|
manual: 'https://docs.maestro.dev/get-started/maestro-mcp',
|
|
214
316
|
detect: { bins: ['maestro'], mcp: ['maestro'] },
|
|
@@ -220,6 +322,12 @@ export const TOOLBOX = Object.freeze([
|
|
|
220
322
|
{
|
|
221
323
|
id: 'deeptutor', name: 'DeepTutor', tier: 'connector', role: 'learning tool: tutors a team member on what is being built',
|
|
222
324
|
licence: 'Apache-2.0', source: 'https://github.com/HKUDS/DeepTutor', checked: CHECKED,
|
|
325
|
+
vetted: {
|
|
326
|
+
on: VETTED_ON, licenceFrom: 'https://github.com/HKUDS/DeepTutor/blob/main/LICENSE',
|
|
327
|
+
source: 'https://github.com/HKUDS/DeepTutor', release: 'v1.6.12',
|
|
328
|
+
packages: [{ registry: 'pypi', name: 'deeptutor', version: '1.6.12' }],
|
|
329
|
+
note: "PyPI's deeptutor lists no project links; DeepTutor's own README names `pip install -U deeptutor`, which is the link to the source.",
|
|
330
|
+
},
|
|
223
331
|
install: [{ type: 'python', command: 'pip install -U deeptutor, then deeptutor init' }],
|
|
224
332
|
manual: null,
|
|
225
333
|
detect: { bins: ['deeptutor'] },
|
|
@@ -263,13 +371,89 @@ export function toolProblems(t) {
|
|
|
263
371
|
}
|
|
264
372
|
need(t?.versions === null || (typeof t?.versions === 'string' && parseRange(t.versions) !== null), 'versions is null or a range this reader understands');
|
|
265
373
|
need(typeof t?.fallback === 'string' && t.fallback.length > 0, 'fallback says what yadflow does without it');
|
|
374
|
+
// A skill's section adds the full stop (E87), so a fallback ending in one would print two.
|
|
375
|
+
need(typeof t?.fallback !== 'string' || !/[.\s]$/.test(t.fallback), 'fallback ends without a full stop or a space');
|
|
266
376
|
need(t?.records === null || (typeof t?.records === 'string' && t.records.length > 0), 'records is null or the line a skill writes');
|
|
267
377
|
need(t?.note === null || (typeof t?.note === 'string' && t.note.length > 0), 'note is null or a sentence');
|
|
268
378
|
need(t?.manual === null || (typeof t?.manual === 'string' && /^https:\/\//.test(t.manual)), 'manual is null or an https URL');
|
|
269
379
|
need(isStringList(t?.usedBy), 'usedBy lists the skills or commands that use it');
|
|
380
|
+
need(t?.tier !== 'connector' || !isStringList(t?.usedBy) || connectorFile(t) !== null, 'a connector names the yad-connect-* skill that connects it');
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Every way a SHIPPED entry's vetting record is incomplete, as sentences (E88). [] means vetted. Kept out of
|
|
385
|
+
// `toolProblems` because a team's own tool is well formed without one: yad vets only what it ships.
|
|
386
|
+
export function vettingProblems(t) {
|
|
387
|
+
const out = [];
|
|
388
|
+
const need = (ok, what) => { if (!ok) out.push(what); };
|
|
389
|
+
const v = t?.vetted;
|
|
390
|
+
need(v && typeof v === 'object' && !Array.isArray(v), 'vetted is set — a tool ships only once its licence and source are vetted');
|
|
391
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return out;
|
|
392
|
+
need(isCalendarDay(v.on), 'vetted.on is a real YYYY-MM-DD date');
|
|
393
|
+
need(typeof v.licenceFrom === 'string' && HTTPS_URL.test(v.licenceFrom), 'vetted.licenceFrom is the https URL the licence was read from');
|
|
394
|
+
need(typeof v.source === 'string' && HTTPS_URL.test(v.source), 'vetted.source is the https URL of the official home');
|
|
395
|
+
need(v.release === null || (typeof v.release === 'string' && v.release.length > 0), 'vetted.release is null or the release tag on that day');
|
|
396
|
+
need(Array.isArray(v.packages), 'vetted.packages is a list');
|
|
397
|
+
for (const p of Array.isArray(v.packages) ? v.packages : []) {
|
|
398
|
+
need(VET_REGISTRIES.includes(p?.registry), `a vetted package's registry is one of ${VET_REGISTRIES.join(', ')}`);
|
|
399
|
+
need(typeof p?.name === 'string' && p.name.length > 0, 'a vetted package has its name');
|
|
400
|
+
need(parseVersion(p?.version)?.full === true, 'a vetted package has the full version that was vetted (1.2.3)');
|
|
401
|
+
}
|
|
402
|
+
need(v.note === null || (typeof v.note === 'string' && v.note.length > 0), 'vetted.note is null or a sentence');
|
|
403
|
+
// A closed tool has no LICENSE file to read, so its proof must be the vendor's terms, not a repo file.
|
|
404
|
+
need(t.licence !== 'proprietary' || !isRepoLicenceFile(v.licenceFrom), 'a proprietary tool\'s licence proof is its terms page');
|
|
270
405
|
return out;
|
|
271
406
|
}
|
|
272
407
|
|
|
408
|
+
// An https URL with a host, not just the scheme.
|
|
409
|
+
const HTTPS_URL = /^https:\/\/[^/\s]+\.[^/\s]+/;
|
|
410
|
+
// Is this URL a licence FILE in a code repository (LICENSE, license, LICENSE-MIT, License.md, COPYING)?
|
|
411
|
+
// Decided by WHERE the URL points, not by its letter case (review 4): a repository file is shown under a
|
|
412
|
+
// `/blob/` or `/raw/` path (GitHub, GitLab's `/-/blob/`, Bitbucket, Codeberg, most self-hosted forges) or
|
|
413
|
+
// served from raw.githubusercontent.com. A repository's front page (`github.com/org/license-tool`) is not
|
|
414
|
+
// a file, and a vendor's terms page at `/license` or `/license-agreement` is the right proof for a closed
|
|
415
|
+
// tool. The query and fragment are not the file; a percent-encoded name is read decoded. `new URL`
|
|
416
|
+
// already lowercases the host.
|
|
417
|
+
const RAW_HOSTS = new Set(['raw.githubusercontent.com']);
|
|
418
|
+
export function isRepoLicenceFile(url) {
|
|
419
|
+
let u;
|
|
420
|
+
try { u = new URL(String(url)); } catch { return false; }
|
|
421
|
+
let pathname = u.pathname;
|
|
422
|
+
try { pathname = decodeURIComponent(pathname); } catch { /* a stray % — read it as written */ }
|
|
423
|
+
const inRepo = RAW_HOSTS.has(u.hostname) || /\/(blob|raw|src)\//.test(pathname);
|
|
424
|
+
const file = pathname.split('/').filter(Boolean).at(-1) || '';
|
|
425
|
+
return inRepo && /^(licen[cs]e|copying)([-_.].*)?$/i.test(file);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// A YYYY-MM-DD string that names a real day. `Date.parse` rolls an impossible day forward (2026-02-30
|
|
429
|
+
// becomes 2 March) instead of refusing it, so the day has to come back out unchanged.
|
|
430
|
+
export const isCalendarDay = (s) => {
|
|
431
|
+
if (typeof s !== 'string' || !DATE.test(s)) return false;
|
|
432
|
+
const t = Date.parse(`${s}T00:00:00Z`);
|
|
433
|
+
return !Number.isNaN(t) && new Date(t).toISOString().slice(0, 10) === s;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// How many whole days old a vetting record is on `today` (a Date). A parameter, never `new Date()` here,
|
|
437
|
+
// so a test can pass any day and the unit suite never ages by itself.
|
|
438
|
+
export const vettingAge = (t, today) =>
|
|
439
|
+
Math.floor((Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()) - Date.parse(`${t.vetted.on}T00:00:00Z`)) / 86400000);
|
|
440
|
+
|
|
441
|
+
// The shipped entries whose vetting cannot be trusted on `today`: `[{ id, name, on, age, why }]`, where
|
|
442
|
+
// `why` is 'stale' (older than VET_MAX_AGE_DAYS) or 'future' (a date after today — a typo like 2027 for
|
|
443
|
+
// 2026 would otherwise keep a tool out of the gate for a year). The release check refuses to publish
|
|
444
|
+
// while this is not empty (`scripts/vet-check.mjs`).
|
|
445
|
+
export const staleVettings = (today, list = TOOLBOX) => list
|
|
446
|
+
.filter((t) => vettingProblems(t).length === 0)
|
|
447
|
+
.map((t) => ({ id: t.id, name: t.name, on: t.vetted.on, age: vettingAge(t, today) }))
|
|
448
|
+
.filter((s) => s.age > VET_MAX_AGE_DAYS || s.age < 0)
|
|
449
|
+
.map((s) => ({ ...s, why: s.age < 0 ? 'future' : 'stale' }));
|
|
450
|
+
|
|
451
|
+
// The ONE Repomix version yad runs (E88): the npm package version its vetting confirmed. `yad setup`,
|
|
452
|
+
// `yad repo refresh` and the install line use it; the skills write it out as text, and a test holds every
|
|
453
|
+
// npx Repomix command in cli/ and skills/ to this value. Moving up is a re-vet and a yadflow release.
|
|
454
|
+
export const REPOMIX_VERSION = TOOLBOX.find((t) => t.id === 'repomix').vetted.packages
|
|
455
|
+
.find((p) => p.registry === 'npm' && p.name === 'repomix').version;
|
|
456
|
+
|
|
273
457
|
// ---- versions ---------------------------------------------------------------------------------------
|
|
274
458
|
|
|
275
459
|
// `1.2.3`, `v1.2`, `3` → [1,2,3] / [1,2,0] / [3,0,0]; a pre-release (`1.2.3-beta`) is below its release.
|
|
@@ -471,6 +655,8 @@ export function customProblems(t) {
|
|
|
471
655
|
const customRow = (t) => ({
|
|
472
656
|
id: t.id, name: t.name || t.id, tier: 'project', role: t.role,
|
|
473
657
|
licence: null, source: t.source ?? null, checked: null,
|
|
658
|
+
// A team's own tool is the team's to vet (E88): yad vets only the list it ships.
|
|
659
|
+
vetted: null,
|
|
474
660
|
install: t.install ?? [], manual: null,
|
|
475
661
|
detect: Object.fromEntries(Object.values(DETECT_KINDS).filter((k) => t.detect[k]?.length).map((k) => [k, t.detect[k]])),
|
|
476
662
|
versions: null, fallback: t.fallback, records: null, note: null, usedBy: [],
|
|
@@ -533,6 +719,240 @@ export function toolUse(tool, { shipped, connected }) {
|
|
|
533
719
|
return { used: false, because: null };
|
|
534
720
|
}
|
|
535
721
|
|
|
722
|
+
// ---- what a skill does when its tool is missing (E87) ------------------------------------------------
|
|
723
|
+
//
|
|
724
|
+
// Every skill a shipped tool's `usedBy` names carries one section, built here, so its words are the
|
|
725
|
+
// toolbox's words and a test can hold the two together. A skill decides from yad's own answer — one rule
|
|
726
|
+
// for every skill, which already sees a plugin turned off in Claude Code — not from its own guess:
|
|
727
|
+
// core used when `yad toolbox list --json` says `used: true` and the state is `installed` or
|
|
728
|
+
// `available`. A team's `skip` (`yad toolbox remove <id>`) means the fallback, even when the
|
|
729
|
+
// tool is installed.
|
|
730
|
+
// connector used when the Product's design, testing or learning file connects it. The connect skill
|
|
731
|
+
// writes that file and the step skill follows it, as before E87.
|
|
732
|
+
// Recommended tools are bound with `yad skill bind`, so no skill names one.
|
|
733
|
+
export const SKILL_FALLBACK_HEADING = '## When a tool is missing';
|
|
734
|
+
// The section's last line. The section is exactly the lines from its heading to this marker, so a
|
|
735
|
+
// rewrite never has to guess from the Markdown where the section ends (rounds 5 to 9 of the E87 review
|
|
736
|
+
// each found one more way such a guess could cut part of a skill).
|
|
737
|
+
export const SKILL_FALLBACK_END = '<!-- end: When a tool is missing -->';
|
|
738
|
+
const CONNECT_FILES = { 'yad-connect-design': 'designConfig', 'yad-connect-testing': 'testingConfig', 'yad-connect-learning': 'learningConfig' };
|
|
739
|
+
|
|
740
|
+
// The Product file that connects a connector: the one its `yad-connect-*` skill writes.
|
|
741
|
+
export function connectorFile(tool) {
|
|
742
|
+
const skill = tool.usedBy.find((u) => CONNECT_FILES[u]);
|
|
743
|
+
return skill ? PROJECT_FILES[CONNECT_FILES[skill]] : null;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// The skills (folders under skills/, not `yad <command>`s) that use a tool.
|
|
747
|
+
export const skillsUsing = (tool) => tool.usedBy.filter((u) => !u.startsWith('yad '));
|
|
748
|
+
|
|
749
|
+
export function skillFallbackSection(skill, tools = TOOLBOX) {
|
|
750
|
+
const mine = tools.filter((t) => t.tier !== 'recommended' && skillsUsing(t).includes(skill));
|
|
751
|
+
if (!mine.length) return null;
|
|
752
|
+
const core = mine.filter((t) => t.tier !== 'connector');
|
|
753
|
+
const connectors = mine.filter((t) => t.tier === 'connector');
|
|
754
|
+
const lines = [
|
|
755
|
+
SKILL_FALLBACK_HEADING,
|
|
756
|
+
'',
|
|
757
|
+
'<!-- Written from yadflow\'s cli/toolbox.mjs (E87). Change the words there: a test holds this section to it. -->',
|
|
758
|
+
'',
|
|
759
|
+
'No tool below is required. When one is missing, this skill still finishes, using the fallback written',
|
|
760
|
+
'beside it.',
|
|
761
|
+
'',
|
|
762
|
+
];
|
|
763
|
+
// Only a skill with a core tool asks yad: a connector is decided by its Product file, and an answer the
|
|
764
|
+
// skill must then ignore would only confuse it.
|
|
765
|
+
if (core.length) {
|
|
766
|
+
lines.push(
|
|
767
|
+
// Name the tools: "the one without a connector" means nothing in a skill that has no connector.
|
|
768
|
+
`For ${core.map((t) => t.name).join(' and ')}${connectors.length ? ` (${core.length === 1 ? 'the one tool here that is not a connector' : 'the tools here that are not connectors'})` : ''}, decide from yad's answer, not from a guess:`,
|
|
769
|
+
'run `yad toolbox list --json` and read, in its `tools` list, the entry with the tool\'s `id`. Run it',
|
|
770
|
+
'where the tool will run: for work inside a code repo, add `--dir <that repo>`, so the tools installed',
|
|
771
|
+
'there are found.',
|
|
772
|
+
'',
|
|
773
|
+
'- **The team\'s choices.** They come from the Product. If the answer\'s `product` is null, yad found no',
|
|
774
|
+
' Product, so a tool the team chose not to use still shows `used: true`. Tell the person that.',
|
|
775
|
+
'- **When yad cannot see the tool.** yad does not read every way a tool can be set up (an older install',
|
|
776
|
+
' may be missed). If the entry says `missing` but the check in the steps below finds the tool, use',
|
|
777
|
+
' it — but never when the entry says `used: false`.',
|
|
778
|
+
'- **When yad cannot answer** (yad is not installed here, or the command fails), use the check in the',
|
|
779
|
+
' steps below.',
|
|
780
|
+
'',
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
lines.push('Tell the person which tools you used and which fallbacks, and why.', '');
|
|
784
|
+
for (const t of core) {
|
|
785
|
+
lines.push(`- **${t.name}** (\`${t.id}\`). Use it when its entry has \`used: true\` and \`status.state\` is \`installed\` or \`available\`. Otherwise — not found, its plugin turned off in Claude Code, or the team chose not to use it (\`yad toolbox remove ${t.id}\`) — use the fallback.`);
|
|
786
|
+
lines.push(` - Fallback — ${t.fallback}.`);
|
|
787
|
+
if (t.records) lines.push(` - Record \`${t.records}\`. It means the tool was not used, whatever the reason.`);
|
|
788
|
+
}
|
|
789
|
+
for (const t of connectors) {
|
|
790
|
+
const file = connectorFile(t);
|
|
791
|
+
const connect = t.usedBy.find((u) => CONNECT_FILES[u]);
|
|
792
|
+
// The connect skill is the one that decides and writes the file; every other skill follows it.
|
|
793
|
+
lines.push(CONNECT_FILES[skill]
|
|
794
|
+
? `- **${t.name}** (\`${t.id}\`, a connector). This skill connects it, by writing \`${file}\` as the steps below say. The other skills follow that file, not yad's toolbox answer.`
|
|
795
|
+
: `- **${t.name}** (\`${t.id}\`, a connector). Used when \`${file}\` connects it. ${connect} writes that file; follow it, not yad's toolbox answer.`);
|
|
796
|
+
lines.push(` - Fallback — ${t.fallback}.`);
|
|
797
|
+
if (t.records) lines.push(` - Record \`${t.records}\`. It means the tool was not used, whatever the reason.`);
|
|
798
|
+
}
|
|
799
|
+
lines.push('', SKILL_FALLBACK_END);
|
|
800
|
+
return lines.join('\n');
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Where the section is in a skill's text: from its heading down to its end marker, both included. A
|
|
804
|
+
// line inside a fenced code block is never either, so an example that quotes them is skipped. A fence
|
|
805
|
+
// closes only with the same character, at least as many times, and nothing after it (CommonMark).
|
|
806
|
+
// A heading may be indented up to three spaces and end in spaces or closing `#`s, as CommonMark allows.
|
|
807
|
+
// `first`, where a new section goes, is the first `##` heading: a skill's `#` title is above it.
|
|
808
|
+
// `[^]*`, not `.*`: `.` stops at a lone \r or a Unicode line break, and `$` then forces a slow retry.
|
|
809
|
+
const HEADING = /^ {0,3}(#{1,2})(?:[ \t]+([^]*))?$/;
|
|
810
|
+
// Only spaces and tabs are trimmed, as CommonMark does — by hand, since /[ \t]+$/ backtracks.
|
|
811
|
+
const trimSpaces = (s) => { let k = s.length; while (k > 0 && (s[k - 1] === ' ' || s[k - 1] === '\t')) k--; return s.slice(0, k); };
|
|
812
|
+
const headingOf = (line) => {
|
|
813
|
+
const h = HEADING.exec(line);
|
|
814
|
+
if (!h) return null;
|
|
815
|
+
// Cut a closing run of `#`s only when a space or tab comes before it (`## C#` keeps its `#`).
|
|
816
|
+
// Walked back by hand: a pattern for the run of `#`s restarts at each one, which is slow on a long run.
|
|
817
|
+
let text = trimSpaces(h[2] ?? '');
|
|
818
|
+
let j = text.length;
|
|
819
|
+
while (j > 0 && text[j - 1] === '#') j--;
|
|
820
|
+
if (j < text.length && (j === 0 || text[j - 1] === ' ' || text[j - 1] === '\t')) text = trimSpaces(text.slice(0, j));
|
|
821
|
+
return { level: h[1].length, text };
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// Is this line the section's heading, in any spelling the finder accepts (indent, case, trailing spaces
|
|
825
|
+
// or `#`s)? One rule, so the finder and the test that counts copies cannot disagree.
|
|
826
|
+
export function isFallbackHeading(line) {
|
|
827
|
+
const h = headingOf(line.replace(/\r$/, ''));
|
|
828
|
+
return h?.level === 2 && h.text.toLowerCase() === SKILL_FALLBACK_HEADING.slice(3).toLowerCase();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const isFallbackEnd = (line) => trimSpaces(line.replace(/\r$/, '')) === SKILL_FALLBACK_END;
|
|
832
|
+
|
|
833
|
+
function sectionBounds(lines) {
|
|
834
|
+
let fence = null;
|
|
835
|
+
const headings = [];
|
|
836
|
+
const ends = [];
|
|
837
|
+
// A YAML frontmatter block at the top (`---` … `---`, after any byte-order mark) is the skill's
|
|
838
|
+
// metadata, never Markdown: a heading-like line there (a `## comment`, a quoted heading in a YAML
|
|
839
|
+
// value) is neither our heading nor where a new section goes, and a fence there is not one. Only our
|
|
840
|
+
// exact end marker is looked for in it: a section that seems to sit inside (a file opening with a
|
|
841
|
+
// `---` divider) then shows as a marker with no heading, and is refused rather than hidden — so a
|
|
842
|
+
// write never runs a section across the block's closing `---`. A copy without a marker inside such a
|
|
843
|
+
// block reads as metadata, as a YAML loader would read it, and is left alone. The block closes at `---`
|
|
844
|
+
// or `...`, trailing spaces allowed, as common frontmatter readers accept.
|
|
845
|
+
let skip = 0;
|
|
846
|
+
// Only when the next line is a YAML key, quoted or not (every SKILL.md opens `name:`): a `---` divider
|
|
847
|
+
// above a title is Markdown, and treating it as frontmatter could hide a section. A YAML comment is not
|
|
848
|
+
// enough: `# T` is a title too.
|
|
849
|
+
if (trimSpaces(lines[0]?.replace(/^\uFEFF/, '') ?? '') === '---' && /^([A-Za-z_][\w-]*[ \t]*:|["'][^"']+["'][ \t]*:)/.test(lines[1] ?? '')) {
|
|
850
|
+
const close = lines.findIndex((l, i) => i > 0 && (trimSpaces(l) === '---' || trimSpaces(l) === '...'));
|
|
851
|
+
if (close > 0) skip = close + 1;
|
|
852
|
+
}
|
|
853
|
+
let fenceAt = -1;
|
|
854
|
+
lines.forEach((l, i) => {
|
|
855
|
+
if (i < skip) {
|
|
856
|
+
if (isFallbackEnd(l)) ends.push(i);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const f = /^ {0,3}(`{3,}|~{3,})([^]*)$/.exec(l);
|
|
860
|
+
if (fence) {
|
|
861
|
+
if (f && f[1][0] === fence[0] && f[1].length >= fence.length && f[2].trim() === '') fence = null;
|
|
862
|
+
} else if (f && !(f[1][0] === '`' && f[2].includes('`'))) { fence = f[1]; fenceAt = i; }
|
|
863
|
+
else if (isFallbackEnd(l)) ends.push(i);
|
|
864
|
+
else {
|
|
865
|
+
const h = headingOf(l);
|
|
866
|
+
if (h) headings.push({ i, level: h.level, ours: isFallbackHeading(l) });
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
const ours = headings.filter((h) => h.ours).map((h) => h.i);
|
|
870
|
+
const at = ours[0];
|
|
871
|
+
const end = at === undefined ? undefined : ends.find((i) => i > at);
|
|
872
|
+
// Why a section cannot be rewritten safely, and what to do, or null. Without its marker, where the
|
|
873
|
+
// section ends is a guess; behind a fence that never closes, nothing after it can be seen at all.
|
|
874
|
+
const problem = fence !== null ? `a code fence is never closed (it opens on line ${fenceAt + 1}), so yad cannot see past it — close it`
|
|
875
|
+
: ours.length > 1 ? 'the section\'s heading is there more than once — delete one of the copies (one without an end marker, if there is one), from its heading down to the end of its text'
|
|
876
|
+
: ends.length > 1 ? 'the section\'s end marker is there more than once — delete the extra marker line'
|
|
877
|
+
: at === undefined ? (ends.length ? 'the end marker is there without the section\'s heading — delete that marker line' : null)
|
|
878
|
+
: end === undefined ? 'the section has no end marker (a copy made by hand, or older than the marker) — delete it, from its heading down to the end of its text' : null;
|
|
879
|
+
return { at, end, problem, first: headings.find((h) => h.level === 2)?.i };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Why yad will not rewrite this skill's section and what to do about it, or null when it can (or there
|
|
883
|
+
// is none).
|
|
884
|
+
export function skillSectionProblem(text) {
|
|
885
|
+
return sectionBounds(text.replace(/\r\n/g, '\n').split('\n')).problem;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Bring every skill under `skillsDir` in line with the toolbox: the section where a tool names the
|
|
889
|
+
// skill, none where no tool does. `check` changes nothing. Returns the skill folder names it found
|
|
890
|
+
// stale and wrote, and `refused: [{ skill, reason }]` — a section it will not touch, and why.
|
|
891
|
+
export function syncSkillFallbacks(skillsDir, { check = false, tools = TOOLBOX } = {}) {
|
|
892
|
+
const out = { stale: [], wrote: [], refused: [] };
|
|
893
|
+
const skills = fs.readdirSync(skillsDir).filter((s) => fs.existsSync(path.join(skillsDir, s, 'SKILL.md'))).sort();
|
|
894
|
+
for (const skill of skills) {
|
|
895
|
+
const file = path.join(skillsDir, skill, 'SKILL.md');
|
|
896
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
897
|
+
const want = skillFallbackSection(skill, tools); // null: no tool names this skill, so no section
|
|
898
|
+
const reason = skillSectionProblem(text);
|
|
899
|
+
// Compared as text, not bytes, so a CRLF checkout (Windows) is not stale for its line endings alone.
|
|
900
|
+
if (!reason && skillSectionOf(text) === want) continue;
|
|
901
|
+
out.stale.push(skill);
|
|
902
|
+
const next = reason ? null : withSkillSection(text, want);
|
|
903
|
+
// A last check that ignores fences, frontmatter and indent: after a write the file holds exactly one
|
|
904
|
+
// `##` line that is the heading and one marker line (none after a removal), counted at any indent,
|
|
905
|
+
// fenced or not. So a copy the finder skipped (fenced, indented in a list) cannot be doubled. It does
|
|
906
|
+
// not catch a copy in another heading form — in a `>` quote, underlined with `---`, or as `<h2>`.
|
|
907
|
+
const seen = next === null ? [] : next.split(/\r?\n/).map((l) => l.replace(/^[ \t]+/, ''));
|
|
908
|
+
const expect = want === null ? 0 : 1;
|
|
909
|
+
const doubled = next !== null && (seen.filter(isFallbackHeading).length !== expect || seen.filter(isFallbackEnd).length !== expect);
|
|
910
|
+
const why = reason || (doubled ? 'after the write the file would hold another line that looks like the section\'s heading or end marker (in a code block, say) — delete or reword it' : null);
|
|
911
|
+
// One answer for both modes: `check` names what a write would refuse.
|
|
912
|
+
if (why) out.refused.push({ skill, reason: why });
|
|
913
|
+
if (check || why) continue;
|
|
914
|
+
fs.writeFileSync(file, next);
|
|
915
|
+
out.wrote.push(skill);
|
|
916
|
+
}
|
|
917
|
+
return out;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// By hand: /\n+$/ restarts at every newline of a long blank run in the middle of the text.
|
|
921
|
+
const dropTrailingNewlines = (s) => { let k = s.length; while (k > 0 && s[k - 1] === '\n') k--; return s.slice(0, k); };
|
|
922
|
+
|
|
923
|
+
// The section as the skill holds it (LF), heading to end marker, or null when it has none. With a
|
|
924
|
+
// problem (no marker, two copies), null too: there is no section yad can read whole.
|
|
925
|
+
export function skillSectionOf(text) {
|
|
926
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
927
|
+
const { at, end, problem } = sectionBounds(lines);
|
|
928
|
+
return at === undefined || problem ? null : lines.slice(at, end + 1).join('\n');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// The skill's text with `section` in place of its old one, or, when it has none, just before its first
|
|
932
|
+
// `## ` heading, so the agent reads it early. `null` removes it, with the blank line it was written
|
|
933
|
+
// with. A section with a problem is left as it is. The file keeps its own line ending.
|
|
934
|
+
export function withSkillSection(text, section) {
|
|
935
|
+
// A file keeps the ending most of its lines have (a Windows checkout is all CRLF).
|
|
936
|
+
const crlf = (text.match(/\r\n/g) || []).length;
|
|
937
|
+
const eol = crlf > (text.match(/\n/g) || []).length - crlf ? '\r\n' : '\n';
|
|
938
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
|
939
|
+
const { at, end, first, problem } = sectionBounds(lines);
|
|
940
|
+
if (problem) return text;
|
|
941
|
+
let out;
|
|
942
|
+
if (at !== undefined) {
|
|
943
|
+
// A removal also takes the blank line yad wrote below it — when a blank line sits above too, or a
|
|
944
|
+
// heading comes next (yad inserts just above one), so the text on either side never closes up.
|
|
945
|
+
let after = end + 1;
|
|
946
|
+
if (section === null && lines[after] === '' && (at === 0 || lines[at - 1] === '' || headingOf(lines[after + 1] ?? '') !== null)) after++;
|
|
947
|
+
out = [...lines.slice(0, at), ...(section === null ? [] : section.split('\n')), ...lines.slice(after)];
|
|
948
|
+
} else if (section === null) out = lines;
|
|
949
|
+
else if (first === undefined) {
|
|
950
|
+
const body = dropTrailingNewlines(lines.join('\n'));
|
|
951
|
+
out = body ? [...body.split('\n'), '', ...section.split('\n'), ''] : [...section.split('\n'), ''];
|
|
952
|
+
} else out = [...lines.slice(0, first), ...section.split('\n'), '', ...lines.slice(first)];
|
|
953
|
+
return out.join('\n').replace(/\n/g, eol);
|
|
954
|
+
}
|
|
955
|
+
|
|
536
956
|
// Every row — shipped, then the team's own — with what is found here and whether it is in use. Finding
|
|
537
957
|
// reads `detectRoot` (the folder yad runs in: a code repo's own skills count); the choices come from the
|
|
538
958
|
// Product, which may be another folder.
|
|
@@ -569,6 +989,8 @@ export function toolboxLines(rows) {
|
|
|
569
989
|
if (s.inRange === false) lines.push(` ${c.yellow('!')} version ${version} is outside the known-good range ${r.versions} — still used; if it misbehaves, install a version in range`);
|
|
570
990
|
if (s.state === 'missing' || s.state === 'disabled') lines.push(` ${c.dim(`without it: ${clean(r.fallback)}`)}`);
|
|
571
991
|
if (r.note) lines.push(` ${c.dim(r.note)}`);
|
|
992
|
+
// E88: what was vetted and when — the licence, and the day its proof was read.
|
|
993
|
+
if (r.vetted) lines.push(` ${c.dim(`licence ${clean(r.licence)}, vetted ${r.vetted.on}`)}`);
|
|
572
994
|
}
|
|
573
995
|
}
|
|
574
996
|
return lines;
|
|
@@ -582,7 +1004,7 @@ export function runToolboxList(root, { json = false, productRoot = null, tools =
|
|
|
582
1004
|
info(productRoot
|
|
583
1005
|
? '[used here] = in use in this Product; change it with `yad toolbox add <id>` / `yad toolbox remove <id>`'
|
|
584
1006
|
: 'no Product here, so this shows the defaults: the core tools are the ones in use');
|
|
585
|
-
info('`yad toolbox list --json` adds each tool\'s licence, source, install commands and where it was found');
|
|
1007
|
+
info('`yad toolbox list --json` adds each tool\'s licence and its vetting record, source, install commands and where it was found');
|
|
586
1008
|
return undefined;
|
|
587
1009
|
}
|
|
588
1010
|
|