ugly-app 0.1.484 → 0.1.485
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/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/templates/.claude/skills/bot-swarm/SKILL.md +50 -13
- package/templates/.claude/skills/fix-feedback/SKILL.md +22 -0
- package/templates/bots/feedback/run-bot.mjs +86 -42
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.485";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
package/package.json
CHANGED
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.485";
|
|
@@ -181,11 +181,50 @@ This starts Docker, MongoDB, and the server but **without** tsx watch, tsc --wat
|
|
|
181
181
|
|
|
182
182
|
**Use the shipped driver — do NOT write your own.** The template includes a pre-validated Playwright driver at `bots/feedback/run-bot.mjs` that handles everything mechanical: launching headless Chromium, setting the bot auth cookie, screenshotting + element-mapping each page, and submitting via `npx ugly-app feedback:submit`. Earlier cycles wasted 20+ minutes mid-run rebuilding this from scratch and debugging the playwright spawn — the bundled script is the contract. Don't rewrite it.
|
|
183
183
|
|
|
184
|
+
The driver has three modes; in this step we use `--capture` then `--submit` so the persona can **actually see the page** before authoring findings:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
node bots/feedback/run-bot.mjs <slug> --capture # crawl + screenshots only
|
|
188
|
+
node bots/feedback/run-bot.mjs <slug> --submit # post findings.json (no Playwright)
|
|
189
|
+
```
|
|
190
|
+
|
|
184
191
|
Dispatch ALL active personas as **parallel subagents** (`delegate_parallel`). Each subagent does:
|
|
185
192
|
|
|
186
|
-
1.
|
|
193
|
+
1. **READ persona.** Open `bots/feedback/active/{slug}/persona.md` and `memory.md` to get into character.
|
|
187
194
|
|
|
188
|
-
|
|
195
|
+
Optionally override which routes the persona crawls by writing `bots/feedback/active/{slug}/pages.json` (default: `["/"]`):
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
["/", "/inbox", "/settings"]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
2. **CAPTURE current state.** Run the driver in capture-only mode:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
node bots/feedback/run-bot.mjs {slug} --capture
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This writes fresh PNGs + element maps to `bots/journal/cycles/screenshots/{slug}/<page>.png` and `<page>.element-map.json` reflecting the CURRENT site. No feedback is submitted yet.
|
|
208
|
+
|
|
209
|
+
3. **LOOK at each page.** For each PNG under `bots/journal/cycles/screenshots/{slug}/`, inspect it using vision — the persona **must actually see** the page before writing findings:
|
|
210
|
+
|
|
211
|
+
- If `analyze_image` is available (ugly-studio coding-agent):
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
analyze_image(
|
|
215
|
+
path="bots/journal/cycles/screenshots/{slug}/<page>.png",
|
|
216
|
+
query="<persona-specific question about layout, contrast,
|
|
217
|
+
copy, mobile fit, image rendering, overlap, etc.>"
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Run multiple targeted queries per image — `analyze_image` is bounded (Haiku) and each call is cheap. Vague one-shots like "describe everything" waste tokens; specific questions like "is the hero CTA legible against the background" / "list any overlapping text in the masthead" / "what does the empty-library state actually look like" produce usable answers.
|
|
222
|
+
|
|
223
|
+
- If running under claude-code CLI or any vision-capable model: Read the PNG path with the Read tool — the model sees it inline.
|
|
224
|
+
|
|
225
|
+
- Cross-reference with the matching `<page>.element-map.json` to resolve specific `dataId` / source-file references when you find something worth reporting.
|
|
226
|
+
|
|
227
|
+
4. **AUTHOR findings.json.** Write 1–5 findings to `bots/feedback/active/{slug}/findings.json`:
|
|
189
228
|
|
|
190
229
|
```json
|
|
191
230
|
[
|
|
@@ -195,23 +234,17 @@ Dispatch ALL active personas as **parallel subagents** (`delegate_parallel`). Ea
|
|
|
195
234
|
]
|
|
196
235
|
```
|
|
197
236
|
|
|
198
|
-
|
|
237
|
+
Each `message` must reference something the persona actually saw — layout reality, contrast, copy, image rendering, overlap, broken animation. Vague feature wishes that could have been written without looking at the page are wasted feedback; rewrite them with a visual hook ("when I see X on /capture, Y is wrong because…").
|
|
199
238
|
|
|
200
|
-
|
|
201
|
-
["/", "/inbox", "/settings"]
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
3. **Run the driver.** This launches Chromium, screenshots each route into `bots/journal/cycles/screenshots/{slug}/<page>.png`, captures the element map, and submits one feedback per finding via the framework's `submitFeedbackBot` endpoint:
|
|
239
|
+
5. **SUBMIT findings.** Run the driver in submit-only mode:
|
|
205
240
|
|
|
206
241
|
```bash
|
|
207
|
-
node bots/feedback/run-bot.mjs {slug}
|
|
242
|
+
node bots/feedback/run-bot.mjs {slug} --submit
|
|
208
243
|
```
|
|
209
244
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
**Be ambitious.** Don't just report cosmetic nits — request new features, suggest adding entire pages, propose redesigns, ask for search/filter/sort, animations, dark mode, better navigation, new data models. Think like a demanding product manager, not a passive observer.
|
|
245
|
+
This posts one feedback row per finding via `submitFeedbackBot` (data-proxy `db.captureFeedback`) with the captured screenshot + element map attached as `context.screenshotPath` / `context.elementMap`.
|
|
213
246
|
|
|
214
|
-
|
|
247
|
+
6. **Update memory.** Edit `bots/feedback/active/{slug}/memory.md`:
|
|
215
248
|
|
|
216
249
|
```markdown
|
|
217
250
|
## Cycle [YYYY-MM-DD]
|
|
@@ -224,6 +257,8 @@ Dispatch ALL active personas as **parallel subagents** (`delegate_parallel`). Ea
|
|
|
224
257
|
[compress entries older than 3 cycles into History block when > ~4000 tokens]
|
|
225
258
|
```
|
|
226
259
|
|
|
260
|
+
**Be ambitious.** Don't just report cosmetic nits — request new features, suggest adding entire pages, propose redesigns, ask for search/filter/sort, animations, dark mode, better navigation, new data models. Think like a demanding product manager, not a passive observer.
|
|
261
|
+
|
|
227
262
|
**Timeout**: 20 minutes per bot. Failed bots do not block others.
|
|
228
263
|
Record which bots succeeded and which failed/timed out.
|
|
229
264
|
|
|
@@ -248,6 +283,8 @@ Run sequentially in one session. No questions. Make autonomous decisions.
|
|
|
248
283
|
npx ugly-app feedback:resolve --id "<feedbackReportId>" --status resolved --resolution "captured for processing"
|
|
249
284
|
```
|
|
250
285
|
|
|
286
|
+
- **Do NOT muzzle this CLI with `2>/dev/null > /dev/null`.** If a resolve call fails, you need to see it. A silent failure here leaves feedback at `status: new`, so the next cycle re-processes the same items as duplicates and the persona memory diverges from reality. If you batch resolves in a loop, treat any non-zero exit as a cycle-breaking error: surface it, stop the loop, and do not claim `Maintain: success`.
|
|
287
|
+
|
|
251
288
|
3. Invoke `/fix-feedback local` — run completely unattended:
|
|
252
289
|
- Do not ask which environment. Use `local`.
|
|
253
290
|
|
|
@@ -36,6 +36,28 @@ The only valid reason to decline feedback is if it contradicts a Critical Rule i
|
|
|
36
36
|
|
|
37
37
|
If you're unsure how to implement something, read the existing code, read the ugly-app API reference, and figure it out. Do not skip it. Do not defer it. Do not say "this would require significant refactoring" — just do the refactoring.
|
|
38
38
|
|
|
39
|
+
## Using screenshots
|
|
40
|
+
|
|
41
|
+
Bot-submitted feedback (and any feedback from the studio's bug-report flow) includes `context.screenshotPath` — an absolute path to the PNG the user/persona saw when they wrote the feedback. **Look at it before proposing a fix.** The written message alone is insufficient context for layout, contrast, spacing, image-rendering, or overlap work — the user is reacting to what they SAW.
|
|
42
|
+
|
|
43
|
+
- If `analyze_image` is available (ugly-studio coding-agent):
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
analyze_image(
|
|
47
|
+
path=context.screenshotPath,
|
|
48
|
+
query="describe the area the user is reacting to; list any visible
|
|
49
|
+
defects in layout, contrast, copy, or image rendering"
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run multiple targeted queries when the feedback is non-trivial — one for the affected region, one to OCR any visible text the user references, one for colors/contrast if the complaint is design-related. `analyze_image` is Haiku-cheap; vague one-shots waste tokens.
|
|
54
|
+
|
|
55
|
+
- If the active model is vision-capable (Claude Code CLI, Claude/GPT/Gemini framework models): `Read(context.screenshotPath)` inlines the image directly.
|
|
56
|
+
|
|
57
|
+
Cross-reference what you see with `context.elementMap` (next section) to translate visual descriptions into source-file references.
|
|
58
|
+
|
|
59
|
+
**Do NOT propose a visual fix without inspecting the screenshot.** Element maps describe DOM state; the screenshot is the only record of what the user perceived.
|
|
60
|
+
|
|
39
61
|
## Using element maps
|
|
40
62
|
|
|
41
63
|
Feedback may include an `elementMap` field — a JSON snapshot of every interactive element on the page with:
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Pre-validated feedback-bot driver. The bot-swarm skill calls this
|
|
4
|
-
* once per persona during Step 2 (Feedback Bots).
|
|
4
|
+
* once or twice per persona during Step 2 (Feedback Bots).
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* node bots/feedback/run-bot.mjs <persona-slug>
|
|
7
|
+
* node bots/feedback/run-bot.mjs <persona-slug> # legacy: capture + submit in one pass
|
|
8
|
+
* node bots/feedback/run-bot.mjs <persona-slug> --capture # crawl + screenshots only (no submit)
|
|
9
|
+
* node bots/feedback/run-bot.mjs <persona-slug> --submit # submit findings.json (no Playwright)
|
|
10
|
+
*
|
|
11
|
+
* The two-phase form exists so the persona subagent can LOOK at the
|
|
12
|
+
* just-captured screenshots (Read on PNG, or `analyze_image`) before
|
|
13
|
+
* authoring findings.json. The single-pass form is the original
|
|
14
|
+
* crawl+submit flow and is kept for callers that don't need vision.
|
|
8
15
|
*
|
|
9
16
|
* Reads `bots/feedback/active/<slug>/.env` for `BOT_TOKEN`, launches a
|
|
10
17
|
* headless Chromium, browses the local dev server (`BASE_URL`,
|
|
11
18
|
* default http://localhost:5400), captures a screenshot per page +
|
|
12
|
-
* an element map, and submits
|
|
19
|
+
* an element map, and submits one `feedback` entry per finding via
|
|
13
20
|
* `npx ugly-app feedback:submit`.
|
|
14
21
|
*
|
|
15
22
|
* Why this is shipped instead of asked-of-the-model: every prior
|
|
16
23
|
* cycle the manager re-invented this script and burned 20+ min
|
|
17
24
|
* iterating on the playwright spawn, the screenshot path layout,
|
|
18
25
|
* and the feedback-submit args. A known-good template short-circuits
|
|
19
|
-
* that loop.
|
|
20
|
-
* looks at, but leave the I/O scaffolding alone.
|
|
26
|
+
* that loop. Don't rewrite the I/O scaffolding.
|
|
21
27
|
*
|
|
22
28
|
* Output:
|
|
23
29
|
* - bots/journal/cycles/screenshots/<slug>/<page>.png
|
|
@@ -35,7 +41,19 @@ const execFileP = promisify(execFile);
|
|
|
35
41
|
|
|
36
42
|
const SLUG = process.argv[2];
|
|
37
43
|
if (!SLUG) {
|
|
38
|
-
console.error(
|
|
44
|
+
console.error(
|
|
45
|
+
'Usage: node bots/feedback/run-bot.mjs <persona-slug> [--capture|--submit]',
|
|
46
|
+
);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const MODE_FLAG = process.argv[3];
|
|
51
|
+
let MODE; // 'capture' | 'submit' | 'both'
|
|
52
|
+
if (!MODE_FLAG) MODE = 'both';
|
|
53
|
+
else if (MODE_FLAG === '--capture') MODE = 'capture';
|
|
54
|
+
else if (MODE_FLAG === '--submit') MODE = 'submit';
|
|
55
|
+
else {
|
|
56
|
+
console.error(`[${SLUG}] unknown mode flag "${MODE_FLAG}" — use --capture or --submit`);
|
|
39
57
|
process.exit(2);
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -162,49 +180,34 @@ async function submitFeedback({ type, message, url, screenshot, elementMap }) {
|
|
|
162
180
|
}
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
// ---
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const browser = await chromium.launch({ headless: true });
|
|
169
|
-
const ctx = await browser.newContext();
|
|
170
|
-
// Cookie-based bot auth: the framework's authReq handler accepts
|
|
171
|
-
// `Authorization: Bearer ...` headers. Set the token as a cookie too
|
|
172
|
-
// so the SPA's same-origin fetches inherit it without each persona
|
|
173
|
-
// having to wire it through manually.
|
|
174
|
-
await ctx.addCookies([
|
|
175
|
-
{
|
|
176
|
-
name: 'ugly_auth',
|
|
177
|
-
value: BOT_TOKEN,
|
|
178
|
-
url: BASE_URL,
|
|
179
|
-
},
|
|
180
|
-
]);
|
|
181
|
-
|
|
182
|
-
const page = await ctx.newPage();
|
|
183
|
-
|
|
184
|
-
for (const route of pages) {
|
|
183
|
+
// --- Per-route helpers ----------------------------------------------
|
|
184
|
+
function routeFilenames(route) {
|
|
185
185
|
const slugForFile = route === '/' ? 'home' : route.replace(/[^a-z0-9]+/gi, '-');
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
return {
|
|
187
|
+
slugForFile,
|
|
188
|
+
screenshotPath: path.join(SCREENSHOTS_DIR, `${slugForFile}.png`),
|
|
189
|
+
elementMapPath: path.join(SCREENSHOTS_DIR, `${slugForFile}.element-map.json`),
|
|
190
|
+
url: `${BASE_URL}${route}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
189
193
|
|
|
194
|
+
async function captureRoute(page, route) {
|
|
195
|
+
const { screenshotPath, elementMapPath, url } = routeFilenames(route);
|
|
190
196
|
try {
|
|
191
197
|
await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 });
|
|
192
198
|
} catch (err) {
|
|
193
199
|
console.error(`[${SLUG}] failed to load ${url}: ${err.message}`);
|
|
194
|
-
|
|
200
|
+
return false;
|
|
195
201
|
}
|
|
196
|
-
|
|
197
202
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
198
203
|
const elementMap = await captureElementMap(page);
|
|
199
204
|
fs.writeFileSync(elementMapPath, JSON.stringify(elementMap, null, 2));
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
200
207
|
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
// `{ type, message }` objects that the persona drafted from
|
|
205
|
-
// looking at the screenshot + element map. Default behavior:
|
|
206
|
-
// submit a single placeholder so the journal isn't empty even if
|
|
207
|
-
// the persona's findings.json wasn't generated.
|
|
208
|
+
function loadFindings(route) {
|
|
209
|
+
// The persona authors findings.json between `--capture` and `--submit`.
|
|
210
|
+
// If absent or empty we emit a placeholder so the journal isn't empty.
|
|
208
211
|
const findingsPath = path.join(BOT_DIR, 'findings.json');
|
|
209
212
|
let findings = [];
|
|
210
213
|
if (fs.existsSync(findingsPath)) {
|
|
@@ -223,17 +226,58 @@ for (const route of pages) {
|
|
|
223
226
|
},
|
|
224
227
|
];
|
|
225
228
|
}
|
|
229
|
+
return findings;
|
|
230
|
+
}
|
|
226
231
|
|
|
232
|
+
async function submitForRoute(route) {
|
|
233
|
+
const { screenshotPath, elementMapPath, url } = routeFilenames(route);
|
|
234
|
+
// The screenshot/element-map are attached only if the capture phase
|
|
235
|
+
// actually produced them (e.g. when running `--submit` after
|
|
236
|
+
// `--capture`, or in `both` mode).
|
|
237
|
+
const hasScreenshot = fs.existsSync(screenshotPath);
|
|
238
|
+
const hasElementMap = fs.existsSync(elementMapPath);
|
|
239
|
+
const findings = loadFindings(route);
|
|
227
240
|
for (const f of findings) {
|
|
228
241
|
await submitFeedback({
|
|
229
242
|
type: f.type ?? 'feature',
|
|
230
243
|
message: `${personaText.slice(0, 200)}\n\n${f.message}`,
|
|
231
244
|
url,
|
|
232
|
-
screenshot: screenshotPath,
|
|
233
|
-
elementMap: elementMapPath,
|
|
245
|
+
screenshot: hasScreenshot ? screenshotPath : undefined,
|
|
246
|
+
elementMap: hasElementMap ? elementMapPath : undefined,
|
|
234
247
|
});
|
|
235
248
|
}
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
|
|
239
|
-
|
|
251
|
+
// --- Main ------------------------------------------------------------
|
|
252
|
+
fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
253
|
+
|
|
254
|
+
if (MODE === 'capture' || MODE === 'both') {
|
|
255
|
+
const browser = await chromium.launch({ headless: true });
|
|
256
|
+
const ctx = await browser.newContext();
|
|
257
|
+
// Cookie-based bot auth: the framework's authReq handler accepts
|
|
258
|
+
// `Authorization: Bearer ...` headers. Set the token as a cookie too
|
|
259
|
+
// so the SPA's same-origin fetches inherit it without each persona
|
|
260
|
+
// having to wire it through manually.
|
|
261
|
+
await ctx.addCookies([
|
|
262
|
+
{
|
|
263
|
+
name: 'ugly_auth',
|
|
264
|
+
value: BOT_TOKEN,
|
|
265
|
+
url: BASE_URL,
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
const page = await ctx.newPage();
|
|
269
|
+
for (const route of pages) {
|
|
270
|
+
const ok = await captureRoute(page, route);
|
|
271
|
+
if (ok && MODE === 'both') {
|
|
272
|
+
await submitForRoute(route);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
await browser.close();
|
|
276
|
+
console.log(`[${SLUG}] ${MODE === 'both' ? 'done' : 'captured'} — ${pages.length} pages.`);
|
|
277
|
+
} else {
|
|
278
|
+
// MODE === 'submit' — no Playwright needed, just post the findings.
|
|
279
|
+
for (const route of pages) {
|
|
280
|
+
await submitForRoute(route);
|
|
281
|
+
}
|
|
282
|
+
console.log(`[${SLUG}] submitted findings for ${pages.length} routes.`);
|
|
283
|
+
}
|