@jesdi/skills 0.0.2 → 0.0.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jesdi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -139,16 +139,25 @@ def render(result):
139
139
  return "\n".join(lines)
140
140
 
141
141
 
142
- def _gh_json(args):
143
- completed = subprocess.run(args, capture_output=True, text=True, check=True)
142
+ def _gh_json(args, env=None):
143
+ completed = subprocess.run(args, capture_output=True, text=True, check=True,
144
+ env=env)
144
145
  return json.loads(completed.stdout)
145
146
 
146
147
 
148
+ def _project_env():
149
+ """User-owned Projects v2 are invisible to fine-grained PATs, so when
150
+ GH_PROJECT_TOKEN is set, `gh project` calls run with GH_TOKEN swapped to
151
+ it; every other gh call keeps the stored auth (which can read the repo)."""
152
+ token = os.environ.get("GH_PROJECT_TOKEN")
153
+ return {**os.environ, "GH_TOKEN": token} if token else None
154
+
155
+
147
156
  def main(owner, project_number, repo):
148
157
  payload = _gh_json([
149
158
  "gh", "project", "item-list", str(project_number),
150
159
  "--owner", owner, "--format", "json", "--limit", "200",
151
- ])
160
+ ], env=_project_env())
152
161
  project_items = payload.get("items", [])
153
162
  issue_rows = _gh_json([
154
163
  "gh", "issue", "list", "--repo", repo, "--state", "all",
@@ -206,6 +206,38 @@ def test_find_project_meta_nearest_ancestor_wins(tmp_path, monkeypatch):
206
206
  assert rank.find_project_meta() == str(inner_meta)
207
207
 
208
208
 
209
+ def test_project_env_absent_without_token(monkeypatch):
210
+ monkeypatch.delenv("GH_PROJECT_TOKEN", raising=False)
211
+ assert rank._project_env() is None
212
+
213
+
214
+ def test_project_env_swaps_gh_token(monkeypatch):
215
+ monkeypatch.setenv("GH_PROJECT_TOKEN", "classic-tok")
216
+ env = rank._project_env()
217
+ assert env["GH_TOKEN"] == "classic-tok"
218
+
219
+
220
+ def test_main_uses_project_token_only_for_project_call(monkeypatch):
221
+ monkeypatch.setenv("GH_PROJECT_TOKEN", "classic-tok")
222
+ calls = []
223
+
224
+ def fake_run(args, capture_output, text, check, env=None):
225
+ calls.append((args, env))
226
+
227
+ class R:
228
+ stdout = "{\"items\": []}" if args[1] == "project" else "[]"
229
+
230
+ return R()
231
+
232
+ monkeypatch.setattr(rank.subprocess, "run", fake_run)
233
+ rank.main("acme", 1, "acme/private-repo")
234
+ project_call, issue_call = calls
235
+ assert project_call[0][1] == "project"
236
+ assert project_call[1]["GH_TOKEN"] == "classic-tok"
237
+ assert issue_call[0][1] == "issue"
238
+ assert issue_call[1] is None # stored gh auth (repo PAT) stays active
239
+
240
+
209
241
  def test_find_project_meta_errors_clearly_when_absent(tmp_path, monkeypatch):
210
242
  monkeypatch.chdir(tmp_path)
211
243
  with pytest.raises(FileNotFoundError) as exc:
@@ -0,0 +1,228 @@
1
+ ---
2
+ name: pr-visual-diff
3
+ description: Capture before/after screenshots of UI views with Playwright and embed them in a PR. Use this skill whenever creating or preparing a pull request that touches frontend code (components, views, styles, layout, Tailwind classes) — even if the user just says "open a PR" or "create the PR" after UI work, and especially if they mention screenshots, visual diff, before/after, or "show what changed". Also use when the user asks to add screenshots to an existing PR.
4
+ ---
5
+
6
+ # PR Visual Diff
7
+
8
+ Produce before/after screenshots of the views affected by a branch's frontend
9
+ changes, commit them on the PR branch, and embed them in the PR body. Reviewers
10
+ should see what changed visually without checking out the branch.
11
+
12
+ The approach leans on the project's existing Playwright e2e harness with the
13
+ **API mocked**: because the data is stubbed and identical across both runs, any
14
+ pixel difference between "before" and "after" is caused by the code change
15
+ alone — that's the property that makes the comparison trustworthy. Never
16
+ screenshot against a live backend for this purpose.
17
+
18
+ Because every project wires this differently, the skill **discovers** the
19
+ project's setup once and caches it, then reuses the cache on later runs.
20
+
21
+ ## Step 0. Load or build the per-project config
22
+
23
+ The config cache lives beside this skill at
24
+ `<skill-dir>/pr-visual-diff.<slug>.json`, where `<slug>` is the target repo's
25
+ directory name. It is gitignored — local to this machine. Ensure the cache file is ignored in the `.gitignore` of the repo where you install the skill directory (add an entry like `pr-visual-diff.*.json` if it isn't already).
26
+
27
+ - **If the cache exists**, read it and skip to "When NOT to run the capture".
28
+ If it says `{"supported": false}`, tell the user this project has no
29
+ mocked-API harness and create a plain PR (see Preconditions), then stop.
30
+ - **If it does not exist**, run the discovery pass below to build it.
31
+
32
+ ### Discovery pass (first run only)
33
+
34
+ Inspect the target repo and record these values, then write them to the cache
35
+ file as JSON:
36
+
37
+ | Key | How to find it |
38
+ |---|---|
39
+ | `frontendDir` | the dir holding the app + its `src/` (repo root or a subdir like `frontend/`) |
40
+ | `e2eDir`, `specsDir` | the Playwright spec/support tree (look for `playwright.config.*`, a `specs/` dir) |
41
+ | `authFixtureImport`, `authFixtureName`, `authedPageProp` | the support file exporting a `test` fixture that seeds auth; note its import path, the exported name, and the authed-page property (e.g. `authenticatedPage`) |
42
+ | `mockHelperImport`, `mockHelperName` | the helper that stubs `/api` routes with fixture data; its import path + exported name |
43
+ | `port` | the `webServer` port in the Playwright config |
44
+ | `deviceProjects` | the config's `projects` (default to a desktop + a mobile project) |
45
+ | `baseBranch` | the repo's default branch (`main`/`master`; `git symbolic-ref refs/remotes/origin/HEAD`) |
46
+ | `packageManager` | infer from lockfile: `pnpm-lock.yaml`→pnpm, `package-lock.json`→npm, `yarn.lock`→yarn, `bun.lockb`→bun |
47
+ | `screenshotDir` | default `docs/pr-screenshots` |
48
+ | `spinnerSelector` | the app's loading-indicator selector (search for a spinner/`role="status"`/`aria-busy` component) |
49
+
50
+ ### Preconditions gate
51
+
52
+ The discovery pass is also the gate. **If there is no Playwright e2e harness
53
+ that mocks the API** (no mock helper, or no Playwright at all), write
54
+ `{ "supported": false, "reason": "<why>" }` to the cache, tell the user that a
55
+ trustworthy before/after diff isn't possible here (unmocked data would differ
56
+ between runs for reasons other than the code change), and **create a plain PR
57
+ instead** — no screenshots. Then stop.
58
+
59
+ ## When NOT to run the capture
60
+
61
+ If `git diff --name-only $(git merge-base <baseBranch> HEAD)...HEAD` shows no
62
+ changes under `<frontendDir>/src/`, there is nothing visual to diff — create the
63
+ PR normally and say so. Test-only, docs-only, or backend-only diffs don't need
64
+ screenshots.
65
+
66
+ ## Always run the capture in a subagent
67
+
68
+ Steps 2–6 (writing the spec, capturing, **reading every PNG back to eyeball
69
+ it**, committing, embedding) MUST run inside a single dispatched subagent — not
70
+ in the main context. Reading screenshots back is this skill's dominant token
71
+ cost, and those image bytes are worthless to the main thread; keeping them in a
72
+ subagent's throwaway context is the whole savings, with no loss of verification.
73
+
74
+ Main context does only: step 1 (map the diff, get the user's go-ahead), then
75
+ dispatch a subagent with the plan and the config values. The subagent returns a
76
+ compact report: the committed screenshot paths + pinned URLs, the PR-body
77
+ markdown, and any views it flagged. The main context creates/edits the PR from
78
+ that report — it never reads a PNG.
79
+
80
+ ## Workflow
81
+
82
+ ### 1. Map the diff to views, then get the user's go-ahead
83
+
84
+ List changed files against the merge base (`git merge-base <baseBranch> HEAD`).
85
+ For each changed component, trace upward (imports → page/route component → route
86
+ path) to find the route(s) where it renders, and note any interaction needed to
87
+ make it visible (open a modal, select a tab). Classify each view as **changed**
88
+ (existed at the merge base) or **new** (introduced by this branch) — new ones
89
+ get an after-only shot.
90
+
91
+ This capture is heavy (temp worktree, two Playwright passes, a commit pushed to
92
+ the branch, PR body rewritten). ALWAYS confirm with AskUserQuestion before
93
+ running any of it. The question must cover **both** (a) whether to run the whole
94
+ before/after capture at all, and (b) that it will commit the screenshots and
95
+ embed them in the PR. Include the proposed plan: which routes/states, and which
96
+ device projects — default to the config's `deviceProjects`; add more only when
97
+ the change is layout-sensitive. If they decline, create the PR normally and stop.
98
+
99
+ ### 2. Write the capture spec
100
+
101
+ Copy `references/capture-spec-template.ts` from this skill into
102
+ `<specsDir>/__pr-capture__.spec.ts` and substitute the `__PVD_*__` markers from
103
+ the config:
104
+
105
+ - `__PVD_AUTH_FIXTURE_IMPORT__` → `authFixtureImport` (and, if the fixture isn't
106
+ named `test`, adjust the `import { <authFixtureName> as test }` accordingly)
107
+ - `__PVD_MOCK_HELPER_IMPORT__` → `mockHelperImport` (and `mockHelperName` if not
108
+ `mockApi`)
109
+ - `__PVD_SPINNER_SELECTOR__` → `spinnerSelector`
110
+ - the `{ authenticatedPage: page }` destructure → `{ <authedPageProp>: page }`
111
+
112
+ Then fill in the `VIEWS` array. This file is scaffolding: never commit it, and
113
+ delete it when done (it lives in the specs dir, so a forgotten copy would run in
114
+ every future e2e invocation).
115
+
116
+ ### 3. Capture "after" (current branch)
117
+
118
+ From `<frontendDir>`. First make sure nothing is listening on `<port>`
119
+ (`lsof -nP -i :<port>`) and kill any stray server there: if the Playwright
120
+ config has `reuseExistingServer` on locally, a leftover server from another
121
+ worktree would be silently reused and you'd screenshot the *wrong code*.
122
+
123
+ ```bash
124
+ PR_SHOT_DIR=<scratchpad>/after PR_SHOT_LABEL=after \
125
+ <packageManager> exec playwright test <specsDir>/__pr-capture__.spec.ts \
126
+ --project=<deviceProject> [--project=<deviceProject>...]
127
+ ```
128
+
129
+ Playwright boots its own server and tears it down. The template pins
130
+ `deviceScaleFactor: 1` and asserts no spinner is left on screen, so a
131
+ raced/under-mocked view fails the test instead of producing a misleading shot.
132
+ That assertion is a backstop, not a substitute for looking: the subagent still
133
+ Reads each PNG after capture — an empty state, error toast, or wrong content
134
+ passes the spinner check but is still wrong. On a genuine failure, fix the spec
135
+ (add mock stubs, a wait for a stable element, or correct the spinner selector)
136
+ rather than shipping a bad screenshot.
137
+
138
+ ### 4. Capture "before" (merge base, temp worktree)
139
+
140
+ A "before" shot only makes sense for views that already existed. If a view is
141
+ introduced by this branch, capture only its "after" shot and present it as
142
+ **New** (step 6). If *every* captured view is new, skip this whole step.
143
+
144
+ For views that did exist, reconstruct the pre-change app from git:
145
+
146
+ ```bash
147
+ BASE=$(git merge-base <baseBranch> HEAD)
148
+ git worktree add <scratchpad>/pr-before-shots "$BASE"
149
+ cd <scratchpad>/pr-before-shots/<frontendDir> && <packageManager> install
150
+ ```
151
+
152
+ Copy the same filled `__pr-capture__.spec.ts` into that worktree's specs dir,
153
+ then run the same command with `PR_SHOT_DIR=<scratchpad>/before
154
+ PR_SHOT_LABEL=before`. Run it sequentially after the "after" pass — both configs
155
+ use `<port>`.
156
+
157
+ Edge cases:
158
+ - A route that doesn't exist at the base (new view): skip it and label it
159
+ **New** in the PR table.
160
+ - The e2e helpers (auth fixture, mock helper) missing or different at the base:
161
+ adapt the spec copy to what exists there; the goal is the same rendered view,
162
+ not identical spec code.
163
+ - `<packageManager> install` failing on a native binding: rerun once; if it
164
+ persists, report it rather than screenshotting a broken build.
165
+
166
+ Remove the worktree when done:
167
+ `git worktree remove --force <scratchpad>/pr-before-shots` (`--force` because
168
+ install left untracked artifacts).
169
+
170
+ ### 5. Commit the images on the PR branch
171
+
172
+ ```
173
+ <screenshotDir>/<branch-slug>/<view>--<project>--{before|after}.png
174
+ ```
175
+
176
+ `<branch-slug>` = branch name with `/` → `-`. Keep only the shots you'll
177
+ reference. Commit them, delete the temp spec, and push.
178
+
179
+ ### 6. Embed in the PR body
180
+
181
+ After pushing, pin image URLs to the exact commit so they keep rendering after
182
+ later force-pushes or branch deletion:
183
+
184
+ ```bash
185
+ SHA=$(git rev-parse HEAD)
186
+ REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
187
+ # → https://github.com/$REPO/blob/$SHA/<screenshotDir>/<slug>/<file>.png?raw=true
188
+ ```
189
+
190
+ Per view, one table. Put **Before on the left and After on the right** (one
191
+ device per row) so a reviewer sees the original and changed component side by
192
+ side:
193
+
194
+ ```markdown
195
+ ### <View name> (`/route`)
196
+
197
+ | | Before | After |
198
+ |---|---|---|
199
+ | **Desktop** | ![](<blob-url>?raw=true) | ![](<blob-url>?raw=true) |
200
+ | **Mobile** | ![](<blob-url>?raw=true) | ![](<blob-url>?raw=true) |
201
+ ```
202
+
203
+ If the repo is private, GitHub's image proxy may refuse to inline blob URLs in
204
+ the PR body. Add a one-line note under the tables linking to the
205
+ `<screenshotDir>/<slug>/` directory on the branch as a fallback — the images
206
+ always render in the Files-changed tab and in blob view for authenticated
207
+ viewers.
208
+
209
+ The subagent's job ends here: it has pushed the commit and produced the PR-body
210
+ markdown, and returns both. The **main context** creates (or edits) the PR with
211
+ that body, then opens it (`gh pr view --web`) so the user can confirm the embeds
212
+ render.
213
+
214
+ For a **new** view there is no Before, so put a **New view** label in the left
215
+ cell instead of an image:
216
+
217
+ ```markdown
218
+ | | Before | After |
219
+ |---|---|---|
220
+ | **Desktop** | _New view_ | ![](<blob-url>?raw=true) |
221
+ | **Mobile** | _New view_ | ![](<blob-url>?raw=true) |
222
+ ```
223
+
224
+ ## Adding shots to an existing PR
225
+
226
+ Same flow; the only differences: diff against the PR's base branch
227
+ (`gh pr view --json baseRefName`) instead of `<baseBranch>`, and edit the body
228
+ with `gh pr edit --body-file` instead of creating.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * pr-visual-diff capture spec — TEMPORARY SCAFFOLDING, never commit.
3
+ *
4
+ * This is a template. Before running, substitute every __PVD_*__ marker below
5
+ * with the value from this project's discovery cache
6
+ * (<skill-dir>/pr-visual-diff.<slug>.json). The markers exist because
7
+ * each project wires its Playwright auth fixture and API mock differently.
8
+ *
9
+ * Copy the filled result to <specsDir>/__pr-capture__.spec.ts, then run:
10
+ *
11
+ * PR_SHOT_DIR=<abs-out-dir> PR_SHOT_LABEL=after \
12
+ * <packageManager> exec playwright test <specsDir>/__pr-capture__.spec.ts \
13
+ * --project=<deviceProject> [--project=<deviceProject>...]
14
+ *
15
+ * Delete the copy when done — anything left in the specs dir runs on every e2e pass.
16
+ */
17
+ import { expect, type Page } from '@playwright/test';
18
+ // __PVD_AUTH_IMPORT__: import the project's test fixture.
19
+ // From config: import { <authFixtureName> } from '<authFixtureImport>'
20
+ import { test } from '__PVD_AUTH_FIXTURE_IMPORT__';
21
+ // __PVD_MOCK_IMPORT__: import the project's API-mock helper.
22
+ // From config: import { <mockHelperName> } from '<mockHelperImport>'
23
+ import { mockApi } from '__PVD_MOCK_HELPER_IMPORT__';
24
+
25
+ const OUT = process.env.PR_SHOT_DIR;
26
+ const LABEL = process.env.PR_SHOT_LABEL ?? 'after';
27
+ if (!OUT) throw new Error('PR_SHOT_DIR must be set to an absolute output directory');
28
+
29
+ // Selector for this app's loading indicator. From config: <spinnerSelector>.
30
+ const SPINNER = '__PVD_SPINNER_SELECTOR__';
31
+
32
+ // Pin device-pixel-ratio to 1 for every project. Device descriptors (iPhone 14,
33
+ // Pixel 7, iPad) default to DPR 2-3, producing PNGs 4-9x larger for zero review
34
+ // value — and those bytes land in the main context when the shots get read back.
35
+ test.use({ deviceScaleFactor: 1 });
36
+
37
+ interface View {
38
+ /** Slug used in the filename, e.g. 'dashboard' or 'add-position-modal'. */
39
+ name: string;
40
+ /** Route to open, e.g. '/dashboard'. */
41
+ path: string;
42
+ /** Optional interactions to reach the state (open a modal, pick a tab…). */
43
+ prepare?: (page: Page) => Promise<void>;
44
+ /** Full-page scroll capture (default) vs just the viewport. */
45
+ fullPage?: boolean;
46
+ }
47
+
48
+ const VIEWS: View[] = [
49
+ // { name: 'dashboard', path: '/dashboard' },
50
+ // {
51
+ // name: 'add-position-modal',
52
+ // path: '/',
53
+ // prepare: async (page) => {
54
+ // await page.getByRole('button', { name: /add symbol/i }).click();
55
+ // await page.getByRole('dialog').waitFor();
56
+ // },
57
+ // },
58
+ ];
59
+
60
+ for (const view of VIEWS) {
61
+ // __PVD_PAGE_PROP__: destructure the project's authed-page fixture prop.
62
+ // From config: { <authedPageProp>: page }
63
+ test(`pr-capture: ${view.name}`, async ({ authenticatedPage: page }, testInfo) => {
64
+ await mockApi(page);
65
+ await page.goto(view.path);
66
+ await page.waitForLoadState('networkidle');
67
+ await view.prepare?.(page);
68
+ // Guard against shipping a misleading shot: a spinner still on screen means
69
+ // the mock is missing an endpoint or prepare() raced the render. Fail loudly
70
+ // here instead of committing a loading-state PNG.
71
+ await expect(
72
+ page.locator(SPINNER),
73
+ `${view.name}: a spinner is still visible — fix the mock/prepare before capturing`,
74
+ ).toHaveCount(0);
75
+ await page.screenshot({
76
+ path: `${OUT}/${view.name}--${testInfo.project.name}--${LABEL}.png`,
77
+ fullPage: view.fullPage ?? true,
78
+ animations: 'disabled',
79
+ caret: 'hide',
80
+ });
81
+ });
82
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-15T09:33:14.993Z",
3
+ "generatedAt": "2026-07-23T11:12:36.101Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
7
- "version": "0.1.0",
8
- "hash": "sha256-ac26c0313981666dfc143bb6eabbf603e0b02111c13ea543e1c86c2900e40559",
7
+ "version": "0.1.1",
8
+ "hash": "sha256-2a594bc97cd045b92b5771f6c462b861b8adcfaee0135c59820af092bdf02a2f",
9
9
  "description": "Manage a GitHub-Issues-backed backlog and a Projects-v2 work graph via five verbs. Use when the user wants to CAPTURE an idea/research thread (\"add to backlog\", \"capture this\", \"note this idea\", \"log a task\"), TRIAGE the inbox (\"triage the backlog\", \"score the inbox\"), see what to work on NEXT (\"what's next\", \"what should I work on\", \"ranked backlog\", \"next actionable task\"), LAND a finished item (\"mark done\", \"close out #N\"), or SETUP the board the first time. Ranks open issues by impact÷effort, respecting `Blocked by:` edges, so only actionable work surfaces."
10
10
  },
11
11
  {
@@ -31,6 +31,12 @@
31
31
  "version": "0.1.0",
32
32
  "hash": "sha256-916ea397cd74d91c4b6060672088b9c54353c915b98bcaef3514815694842e5a",
33
33
  "description": "Minimal example skill proving the distribution pipeline works. Responds to \"test my skills setup\" by confirming the skill is loaded."
34
+ },
35
+ {
36
+ "name": "pr-visual-diff",
37
+ "version": "0.1.0",
38
+ "hash": "sha256-4e386dd175a34e55e89e0e29e37f5d0afd927560b7e3ba4c5f3ef482fb73f722",
39
+ "description": "Capture before/after screenshots of UI views with Playwright and embed them in a PR. Use this skill whenever creating or preparing a pull request that touches frontend code (components, views, styles, layout, Tailwind classes) — even if the user just says \"open a PR\" or \"create the PR\" after UI work, and especially if they mention screenshots, visual diff, before/after, or \"show what changed\". Also use when the user asks to add screenshots to an existing PR."
34
40
  }
35
41
  ]
36
42
  }