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.
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.484";
1
+ export declare const CLI_VERSION = "0.1.485";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.484";
2
+ export const CLI_VERSION = "0.1.485";
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.484",
3
+ "version": "0.1.485",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.484";
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. Read `bots/feedback/active/{slug}/persona.md` and `memory.md` to get into character.
193
+ 1. **READ persona.** Open `bots/feedback/active/{slug}/persona.md` and `memory.md` to get into character.
187
194
 
188
- 2. **Author the persona's findings.** Look at the screenshots + element maps from the previous cycle if any (`bots/journal/cycles/screenshots/{slug}/*.png` and `*.element-map.json`). Decide what 1–5 observations this persona would make. Write them to `bots/feedback/active/{slug}/findings.json`:
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
- Optionally, override which routes the persona crawls by writing `bots/feedback/active/{slug}/pages.json` (default: `["/"]`):
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
- ```json
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
- 4. Update `bots/feedback/active/{slug}/memory.md` with what you submitted, what changed since last cycle, and how the persona's view of the site is evolving.
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
- 8. Update `bots/feedback/active/{slug}/memory.md`:
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 a `feedback` entry per finding via
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. Edit the `analyze` callback to change what each persona
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('Usage: node bots/feedback/run-bot.mjs <persona-slug>');
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
- // --- Main crawl ------------------------------------------------------
166
- fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
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
- const screenshotPath = path.join(SCREENSHOTS_DIR, `${slugForFile}.png`);
187
- const elementMapPath = path.join(SCREENSHOTS_DIR, `${slugForFile}.element-map.json`);
188
- const url = `${BASE_URL}${route}`;
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
- continue;
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
- // --- Hand-off to the persona's analysis ----------------------------
202
- // Override `analyze` per persona by writing
203
- // `bots/feedback/active/<slug>/findings.json` with an array of
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
- await browser.close();
239
- console.log(`[${SLUG}] done ${pages.length} pages crawled.`);
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
+ }