@iamu-dev/iamu 0.8.0-beta.0 → 0.8.0-beta.1

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/index.js CHANGED
@@ -2534,8 +2534,39 @@ var init_dist = __esm({
2534
2534
 
2535
2535
  // ../../packages/local-store/dist/atomic.js
2536
2536
  import { randomUUID } from "crypto";
2537
- import { closeSync, fsyncSync, mkdirSync, openSync, promises as fs, renameSync, unlinkSync, writeFileSync } from "fs";
2537
+ import fsSync, { closeSync, fsyncSync, mkdirSync, openSync, promises as fs, unlinkSync, writeFileSync } from "fs";
2538
2538
  import path from "path";
2539
+ function isTransientWindowsRenameError(err) {
2540
+ const code = err?.code;
2541
+ return code === "EPERM" || code === "EACCES";
2542
+ }
2543
+ function renameSyncWithRetry(src, dest, maxRetries = MAX_RENAME_RETRIES) {
2544
+ for (let attempt = 0; ; attempt++) {
2545
+ try {
2546
+ fsSync.renameSync(src, dest);
2547
+ return;
2548
+ } catch (err) {
2549
+ if (isTransientWindowsRenameError(err) && attempt < maxRetries) {
2550
+ continue;
2551
+ }
2552
+ throw err;
2553
+ }
2554
+ }
2555
+ }
2556
+ async function renameWithRetry(src, dest, maxRetries = MAX_RENAME_RETRIES) {
2557
+ for (let attempt = 0; ; attempt++) {
2558
+ try {
2559
+ await fs.rename(src, dest);
2560
+ return;
2561
+ } catch (err) {
2562
+ if (isTransientWindowsRenameError(err) && attempt < maxRetries) {
2563
+ await new Promise((resolve3) => setTimeout(resolve3, 15));
2564
+ continue;
2565
+ }
2566
+ throw err;
2567
+ }
2568
+ }
2569
+ }
2539
2570
  function atomicWriteFileSync(targetPath, content, opts = {}) {
2540
2571
  const dir = path.dirname(targetPath);
2541
2572
  const base = path.basename(targetPath);
@@ -2549,7 +2580,7 @@ function atomicWriteFileSync(targetPath, content, opts = {}) {
2549
2580
  fsyncSync(fd);
2550
2581
  closeSync(fd);
2551
2582
  fd = null;
2552
- renameSync(tmpPath, targetPath);
2583
+ renameSyncWithRetry(tmpPath, targetPath);
2553
2584
  renamed2 = true;
2554
2585
  } finally {
2555
2586
  if (fd !== null) {
@@ -2581,7 +2612,7 @@ async function atomicWriteFile(targetPath, content, opts = {}) {
2581
2612
  } finally {
2582
2613
  await handle.close();
2583
2614
  }
2584
- await fs.rename(tmpPath, targetPath);
2615
+ await renameWithRetry(tmpPath, targetPath);
2585
2616
  renamed2 = true;
2586
2617
  } finally {
2587
2618
  if (!renamed2) {
@@ -2603,9 +2634,11 @@ async function appendJsonLine(targetPath, record) {
2603
2634
  await handle.close();
2604
2635
  }
2605
2636
  }
2637
+ var MAX_RENAME_RETRIES;
2606
2638
  var init_atomic = __esm({
2607
2639
  "../../packages/local-store/dist/atomic.js"() {
2608
2640
  "use strict";
2641
+ MAX_RENAME_RETRIES = 4;
2609
2642
  }
2610
2643
  });
2611
2644
 
@@ -10753,6 +10786,7 @@ async function readDesigner(projectRoot, runId, findings2) {
10753
10786
  design_files: raw.design_files ?? [],
10754
10787
  summary: raw.summary ?? "",
10755
10788
  ...raw.skipped ? { skipped: raw.skipped.reason ?? "skipped" } : {},
10789
+ ...raw.screens && raw.screens.length > 0 ? { screens: raw.screens } : {},
10756
10790
  rules_broken: broken,
10757
10791
  rules_held: held,
10758
10792
  issues: (raw.issues ?? []).map((i) => ({
@@ -10821,7 +10855,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
10821
10855
  // Step files record the path relative to the run directory
10822
10856
  // (`screenshots/step-1.png`); older ones recorded the bare file
10823
10857
  // name. Normalize on the file name so the prefix is never doubled.
10824
- screenshot_ref: s.screenshot ? path20.join("screenshots", path20.basename(s.screenshot)) : null,
10858
+ screenshot_ref: s.screenshot ? path20.posix.join("screenshots", path20.basename(s.screenshot)) : null,
10825
10859
  persona_reasoning: s.note,
10826
10860
  grounding: s.grounding ?? null,
10827
10861
  action: s.action,
@@ -15224,6 +15258,13 @@ function renderDesigner(designer2, lines) {
15224
15258
  }
15225
15259
  lines.push("", designerRuleCounts(designer2));
15226
15260
  }
15261
+ if (designer2.proposals.length > 0) {
15262
+ lines.push("", `Proposed doc edits: ${designer2.proposals.map((p) => `\`${p.slug}\` (${p.outcome})`).join(", ")}. Decide them with \`iamu design proposals\`.`);
15263
+ }
15264
+ }
15265
+ function renderDesignerFindings(designer2, lines) {
15266
+ if (designer2.skipped)
15267
+ return;
15227
15268
  const own = designer2.issues.filter((i) => !i.design_cause_of);
15228
15269
  lines.push("", own.length === 0 ? "_No designer findings beyond the design causes above._" : `${designerCount(designer2)}, worst first.`);
15229
15270
  for (const g of designerIssuesByScreen(designer2)) {
@@ -15237,9 +15278,35 @@ function renderDesigner(designer2, lines) {
15237
15278
  lines.push(`- ${i.id} **${i.title}** (${designerIssueMeta(i, (t) => `_${t}_`)}).${text.length > 0 ? ` ${text.join(" ")}` : ""}`);
15238
15279
  }
15239
15280
  }
15240
- if (designer2.proposals.length > 0) {
15241
- lines.push("", `Proposed doc edits: ${designer2.proposals.map((p) => `\`${p.slug}\` (${p.outcome})`).join(", ")}. Decide them with \`iamu design proposals\`.`);
15281
+ }
15282
+ function designerScreenReadingsByStep(designer2, steps2) {
15283
+ const out2 = /* @__PURE__ */ new Map();
15284
+ if (!designer2?.screens || designer2.screens.length === 0)
15285
+ return out2;
15286
+ const byKey = new Map(designer2.screens.filter((s) => s.reading?.trim() || s.serves && s.serves.length > 0).map((s) => [s.key, s]));
15287
+ const shown = /* @__PURE__ */ new Set();
15288
+ for (const st of steps2) {
15289
+ const candidates = [
15290
+ pagePath(st.url),
15291
+ st.surface ?? null,
15292
+ `step-${st.step_number}`
15293
+ ].filter((c) => Boolean(c));
15294
+ for (const key of candidates) {
15295
+ if (shown.has(key))
15296
+ continue;
15297
+ const screen = byKey.get(key);
15298
+ if (screen) {
15299
+ shown.add(key);
15300
+ out2.set(st.step_number, {
15301
+ key: screen.key,
15302
+ reading: screen.reading.trim(),
15303
+ serves: (screen.serves ?? []).map((x) => x.what.trim()).filter(Boolean)
15304
+ });
15305
+ break;
15306
+ }
15307
+ }
15242
15308
  }
15309
+ return out2;
15243
15310
  }
15244
15311
  function renderRunDetails(c, lines) {
15245
15312
  lines.push("", "## Run details", "", `- Run ID: ${c.run_id}`, `- Target URL: ${c.target.url}`);
@@ -15277,12 +15344,13 @@ function renderRunDetails(c, lines) {
15277
15344
  if (diff)
15278
15345
  lines.push("", renderJourneyDiffMarkdown(diff));
15279
15346
  }
15280
- function renderSteps(c, lines) {
15347
+ function renderSteps(c, designer2, lines) {
15281
15348
  lines.push("", "## Steps");
15282
15349
  if (c.steps.length === 0) {
15283
15350
  lines.push("", "_No steps recorded._");
15284
15351
  return;
15285
15352
  }
15353
+ const readings = designerScreenReadingsByStep(designer2, c.steps);
15286
15354
  for (const s of c.steps) {
15287
15355
  lines.push("", `### ${s.step_number}. ${s.screen_title ?? "(untitled)"} \u2014 ${s.id}`);
15288
15356
  const where = [s.surface, s.url].filter(Boolean).join(" \u2014 ");
@@ -15290,6 +15358,16 @@ function renderSteps(c, lines) {
15290
15358
  lines.push("", `Page: ${where}`);
15291
15359
  if (s.persona_reasoning)
15292
15360
  lines.push("", `> ${s.persona_reasoning.replace(/\n/g, "\n> ")}`);
15361
+ const reading = readings.get(s.step_number);
15362
+ if (reading) {
15363
+ const parts = [
15364
+ reading.reading,
15365
+ reading.serves.length > 0 ? `Serves: ${reading.serves.join("; ")}` : ""
15366
+ ].filter(Boolean);
15367
+ if (parts.length > 0) {
15368
+ lines.push("", `**Designer reading** (\`${reading.key}\`): ${parts.join(" ")}`);
15369
+ }
15370
+ }
15293
15371
  if (s.grounding)
15294
15372
  lines.push("", `Grounding: ${s.grounding}`);
15295
15373
  if (s.screenshot_ref)
@@ -15327,7 +15405,10 @@ function renderCanonicalMarkdown(canonical3) {
15327
15405
  const checks = c.checks;
15328
15406
  if (checks)
15329
15407
  renderChecks(c, checks, lines);
15408
+ const designer2 = canonicalDesignerOf(c);
15330
15409
  renderHumanTest(c, lines);
15410
+ if (designer2)
15411
+ renderDesigner(designer2, lines);
15331
15412
  const verification = c.verification;
15332
15413
  const newIds = new Set(verification?.new_findings.map((n) => n.id) ?? []);
15333
15414
  const problems = c.findings.filter((f) => f.type !== "helps" && !isSetupArtifact(f)).sort((a, b) => rank(a.severity, SEVERITY_ORDER) - rank(b.severity, SEVERITY_ORDER) || rank(a.type, TYPE_ORDER) - rank(b.type, TYPE_ORDER));
@@ -15335,7 +15416,6 @@ function renderCanonicalMarkdown(canonical3) {
15335
15416
  if (verification) {
15336
15417
  renderSince(verification, problems.filter((f) => newIds.has(f.id)).length, strengths.filter((f) => newIds.has(f.id)).length, lines);
15337
15418
  }
15338
- const designer2 = canonicalDesignerOf(c);
15339
15419
  const causes = designCausesByFinding(designer2);
15340
15420
  lines.push("", "## Findings");
15341
15421
  if (problems.length === 0) {
@@ -15352,11 +15432,11 @@ function renderCanonicalMarkdown(canonical3) {
15352
15432
  renderFinding(c, f, i + 1, since, causes.get(f.id) ?? [], lines);
15353
15433
  });
15354
15434
  }
15435
+ if (designer2)
15436
+ renderDesignerFindings(designer2, lines);
15355
15437
  const analysis = c.analysis;
15356
15438
  if (analysis)
15357
15439
  renderDirections(analysis, lines);
15358
- if (designer2)
15359
- renderDesigner(designer2, lines);
15360
15440
  if (analysis)
15361
15441
  renderNotes(analysis, lines);
15362
15442
  const what = c.what_worked ?? [];
@@ -15380,7 +15460,7 @@ function renderCanonicalMarkdown(canonical3) {
15380
15460
  for (const n of setup)
15381
15461
  lines.push(`- ${n}`);
15382
15462
  }
15383
- renderSteps(c, lines);
15463
+ renderSteps(c, designer2, lines);
15384
15464
  renderRunDetails(c, lines);
15385
15465
  return `${lines.join("\n")}
15386
15466
  `;
@@ -22704,8 +22784,8 @@ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
22704
22784
  import { fileURLToPath as fileURLToPath2 } from "url";
22705
22785
  import chalk6 from "chalk";
22706
22786
  function getVersion() {
22707
- if ("0.8.0-beta.0") {
22708
- return "0.8.0-beta.0";
22787
+ if ("0.8.0-beta.1") {
22788
+ return "0.8.0-beta.1";
22709
22789
  }
22710
22790
  try {
22711
22791
  const thisDir = dirname3(fileURLToPath2(import.meta.url));
@@ -23649,7 +23729,7 @@ import chalk9 from "chalk";
23649
23729
  function openBrowser(url, run = spawnDetached) {
23650
23730
  try {
23651
23731
  const parsed = new URL(url);
23652
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23732
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" && parsed.protocol !== "file:") {
23653
23733
  return;
23654
23734
  }
23655
23735
  } catch {
@@ -24450,6 +24530,7 @@ function renderRunView(v, width = 100) {
24450
24530
  ` ${verdictColor(t.label)(padRight(t.label, 8))} ${truncate(t.question, w - 14)}`
24451
24531
  );
24452
24532
  }
24533
+ if (v.designer) lines.push(...renderDesignerBlock(v.designer, w));
24453
24534
  if (v.since) {
24454
24535
  const s = v.since;
24455
24536
  const parts = [
@@ -24538,6 +24619,9 @@ function renderRunView(v, width = 100) {
24538
24619
  ` ${padRight(chalk12.green("\u2713"), 3)}${padRight("", 8)} ${padRight(chalk12.green("helps"), 9)} ${truncate(s, titleWidth + whereWidth)}`
24539
24620
  );
24540
24621
  }
24622
+ if (v.designer && !v.designer.skipped) {
24623
+ lines.push(...renderDesignerFindingsBlock(v.designer, w));
24624
+ }
24541
24625
  if (v.directions.length > 0) {
24542
24626
  lines.push("", ` ${chalk12.bold("Directions")}`);
24543
24627
  for (const d of v.directions) {
@@ -24549,7 +24633,6 @@ function renderRunView(v, width = 100) {
24549
24633
  );
24550
24634
  }
24551
24635
  }
24552
- if (v.designer) lines.push(...renderDesignerBlock(v.designer, w));
24553
24636
  if (v.observations.length > 0 || v.setupNotes.length > 0) {
24554
24637
  lines.push("", ` ${chalk12.bold("Also worth knowing")}`);
24555
24638
  for (const n of v.setupNotes) {
@@ -24598,6 +24681,17 @@ function renderDesignerBlock(designer2, w) {
24598
24681
  }
24599
24682
  lines.push(chalk12.dim(` ${designerRuleCounts(designer2)}`));
24600
24683
  }
24684
+ if (designer2.proposals.length > 0) {
24685
+ lines.push(
24686
+ chalk12.dim(
24687
+ ` ${designer2.proposals.length} proposed doc edit${designer2.proposals.length === 1 ? "" : "s"}: iamu design proposals`
24688
+ )
24689
+ );
24690
+ }
24691
+ return lines;
24692
+ }
24693
+ function renderDesignerFindingsBlock(designer2, w) {
24694
+ const lines = [];
24601
24695
  for (const g of designerIssuesByScreen(designer2)) {
24602
24696
  lines.push(` ${g.screen ?? "Across the run"}`);
24603
24697
  for (const i of g.issues) {
@@ -24616,13 +24710,6 @@ function renderDesignerBlock(designer2, w) {
24616
24710
  }
24617
24711
  }
24618
24712
  }
24619
- if (designer2.proposals.length > 0) {
24620
- lines.push(
24621
- chalk12.dim(
24622
- ` ${designer2.proposals.length} proposed doc edit${designer2.proposals.length === 1 ? "" : "s"}: iamu design proposals`
24623
- )
24624
- );
24625
- }
24626
24713
  return lines;
24627
24714
  }
24628
24715
  function renderOverview(v, width = 100) {
@@ -25777,12 +25864,28 @@ function renderDesigner2(designer2) {
25777
25864
  const rules = designer2.judged_against === "design-docs" ? `${designer2.rules_broken.length > 0 ? `<div class="card-label designer-gap">Rules broken</div>
25778
25865
  <ul class="designer-list">${designer2.rules_broken.map((r) => `<li>${esc(designerBreakLine(r))}</li>`).join("")}</ul>` : ""}
25779
25866
  <div class="finding-meta">${esc(designerRuleCounts(designer2))}</div>` : "";
25867
+ const proposals = designer2.proposals.length > 0 ? `<div class="finding-meta designer-gap">${esc(`Proposed doc edits: ${designer2.proposals.map((p) => p.slug).join(", ")}. Decide them with iamu design proposals.`)}</div>` : "";
25868
+ return `
25869
+ <section>
25870
+ <style>${DESIGNER_CSS}
25871
+ </style>
25872
+ <h2>Designer's review</h2>
25873
+ <div class="card">
25874
+ <div class="finding-meta" style="margin-top:0">${esc(designerLabel(designer2))}</div>
25875
+ ${designer2.summary ? `<p style="font-size:14px;line-height:1.6;margin-top:0.4rem">${esc(designer2.summary)}</p>` : ""}
25876
+ ${rules}
25877
+ ${proposals}
25878
+ </div>
25879
+ </section>`;
25880
+ }
25881
+ function renderDesignerFindings2(designer2) {
25882
+ if (designer2.skipped) return "";
25780
25883
  const screens = designerIssuesByScreen(designer2).map(
25781
25884
  (g) => `
25782
25885
  <div class="card-label designer-gap">${esc(g.screen ?? "Across the run")}</div>
25783
25886
  ${g.issues.map(
25784
25887
  (i) => `
25785
- <div class="designer-item">
25888
+ <div class="finding-item designer-item">
25786
25889
  <div class="designer-title">${designerTitle(i)}</div>
25787
25890
  <div class="designer-meta">${esc(designerIssueMeta(i))}</div>
25788
25891
  <div class="designer-text">${esc([i.what, i.why].filter(Boolean).join(" "))}</div>
@@ -25790,19 +25893,18 @@ function renderDesigner2(designer2) {
25790
25893
  </div>`
25791
25894
  ).join("")}`
25792
25895
  ).join("");
25793
- const proposals = designer2.proposals.length > 0 ? `<div class="finding-meta designer-gap">${esc(`Proposed doc edits: ${designer2.proposals.map((p) => p.slug).join(", ")}. Decide them with iamu design proposals.`)}</div>` : "";
25794
25896
  return `
25795
- <section>
25796
- <style>${DESIGNER_CSS}
25797
- </style>
25798
- <h2>Designer's review</h2>
25799
- <div class="finding-meta">${esc(designerCount(designer2))}</div>
25800
- <div class="finding-meta">${esc(designerLabel(designer2))}</div>
25801
- ${designer2.summary ? `<p style="font-size:14px;line-height:1.6">${esc(designer2.summary)}</p>` : ""}
25802
- ${rules}
25803
- ${screens || '<div class="finding-meta">No designer findings beyond the design causes above.</div>'}
25804
- ${proposals}
25805
- </section>`;
25897
+ <div class="finding-meta designer-gap">${esc(designerCount(designer2))}</div>
25898
+ ${screens || '<div class="finding-meta">No designer findings beyond the design causes above.</div>'}`;
25899
+ }
25900
+ function renderStepDesignerReading(reading) {
25901
+ if (!reading || !reading.reading && reading.serves.length === 0) return "";
25902
+ return `
25903
+ <div class="designer-item">
25904
+ <div class="designer-meta">Designer reading \xB7 ${esc(reading.key)}</div>
25905
+ ${reading.reading ? `<div class="designer-text">${esc(reading.reading)}</div>` : ""}
25906
+ ${reading.serves.length > 0 ? `<div class="designer-meta">Serves: ${esc(reading.serves.join("; "))}</div>` : ""}
25907
+ </div>`;
25806
25908
  }
25807
25909
  function generateHtmlReport(data) {
25808
25910
  const {
@@ -25825,6 +25927,14 @@ function generateHtmlReport(data) {
25825
25927
  const dateStr = formatDate(completedAt ?? startedAt);
25826
25928
  const problems = sortProblems(findings2.filter((f) => f.type !== "helps"));
25827
25929
  const causes = designCausesByFinding(data.designer ?? null);
25930
+ const screenReadings = designerScreenReadingsByStep(
25931
+ data.designer ?? null,
25932
+ steps2.map((s) => ({
25933
+ step_number: s.stepNumber,
25934
+ url: s.url,
25935
+ surface: s.screenTitle
25936
+ }))
25937
+ );
25828
25938
  const strengths = findings2.filter((f) => f.type === "helps");
25829
25939
  const criticalCount = problems.filter(
25830
25940
  (f) => f.severity === "critical"
@@ -26344,7 +26454,9 @@ function generateHtmlReport(data) {
26344
26454
  </section>
26345
26455
  ` : ""}
26346
26456
 
26347
- ${verification ? renderSinceTable(verification) : ""}
26457
+ ${data.designer ? `${renderDesigner2(data.designer)}
26458
+
26459
+ ` : ""}${verification ? renderSinceTable(verification) : ""}
26348
26460
 
26349
26461
  <!-- Findings, worst first -->
26350
26462
  ${problems.length > 0 ? `
@@ -26364,13 +26476,16 @@ function generateHtmlReport(data) {
26364
26476
  ${f.recommendation ? `<div class="finding-meta finding-rec"><strong>Recommendation:</strong> ${inlineMarkdown(f.recommendation)}</div>` : ""}${renderDesignCauses2(causes.get(f.id) ?? [])}
26365
26477
  </div>
26366
26478
  `
26367
- ).join("")}
26479
+ ).join("")}${data.designer ? renderDesignerFindings2(data.designer) : ""}
26480
+ </section>
26481
+ ` : data.designer && !data.designer.skipped ? `
26482
+ <section>
26483
+ <h2>Findings (0)</h2>${renderDesignerFindings2(data.designer)}
26368
26484
  </section>
26369
26485
  ` : ""}
26370
26486
 
26371
26487
  ${[
26372
26488
  data.analysis ? renderDirections2(data.analysis) : "",
26373
- data.designer ? renderDesigner2(data.designer) : "",
26374
26489
  data.analysis ? renderNotes2(data.analysis) : ""
26375
26490
  ].filter(Boolean).map((section) => `${section}
26376
26491
 
@@ -26426,7 +26541,7 @@ function generateHtmlReport(data) {
26426
26541
  ${s.personaReasoning ? `
26427
26542
  <div class="step-thought">
26428
26543
  <div class="prose-quote">${esc(s.personaReasoning)}</div>
26429
- </div>` : ""}
26544
+ </div>` : ""}${renderStepDesignerReading(screenReadings.get(s.stepNumber))}
26430
26545
  ${renderStepScreenshotHtml(s)}
26431
26546
  </div>
26432
26547
  </div>
@@ -26648,6 +26763,7 @@ __export(report_exports, {
26648
26763
  report: () => report
26649
26764
  });
26650
26765
  import path48 from "path";
26766
+ import { pathToFileURL } from "url";
26651
26767
  import chalk15 from "chalk";
26652
26768
  async function report(scenario, opts) {
26653
26769
  const ctx = await resolveProject(opts);
@@ -26749,7 +26865,7 @@ async function htmlReport(projectRoot, target, opts) {
26749
26865
  console.log();
26750
26866
  if (opts.open) {
26751
26867
  const { openBrowser: openBrowser2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
26752
- openBrowser2(`file://${filePath}`);
26868
+ openBrowser2(pathToFileURL(filePath).href);
26753
26869
  }
26754
26870
  }
26755
26871
  async function jsonRows(projectRoot, runs) {
@@ -26805,6 +26921,7 @@ __export(open_exports, {
26805
26921
  });
26806
26922
  import { existsSync as existsSync12, promises as fs54 } from "fs";
26807
26923
  import path49 from "path";
26924
+ import { pathToFileURL as pathToFileURL2 } from "url";
26808
26925
  import { isCancel as isCancel3, select as select3 } from "@clack/prompts";
26809
26926
  import chalk16 from "chalk";
26810
26927
  async function openDashboard(runId, opts) {
@@ -26932,7 +27049,7 @@ async function openLocalReport(projectRoot, runId, opts) {
26932
27049
  console.log(` ${symbols.success} Opening local walkthrough report:`);
26933
27050
  console.log(` ${chalk16.cyan(filePath)}`);
26934
27051
  console.log();
26935
- openBrowser(`file://${filePath}`);
27052
+ openBrowser(pathToFileURL2(filePath).href);
26936
27053
  }
26937
27054
  var init_open = __esm({
26938
27055
  "src/commands/open.ts"() {
@@ -29473,7 +29590,7 @@ __export(init_exports, {
29473
29590
  init: () => init,
29474
29591
  setupCommand: () => setupCommand
29475
29592
  });
29476
- import { existsSync as existsSync13, readFileSync as readFileSync6 } from "fs";
29593
+ import { existsSync as existsSync13, readFileSync as readFileSync6, readdirSync as readdirSync3 } from "fs";
29477
29594
  import { mkdir as mkdir4, readdir, writeFile as writeFile3 } from "fs/promises";
29478
29595
  import { basename as basename2, join as join6 } from "path";
29479
29596
  import { isCancel as isCancel4, multiselect, select as select4 } from "@clack/prompts";
@@ -29495,13 +29612,9 @@ function setupCommand(projectRoot, hosts) {
29495
29612
  if (hosts.every((h) => h.value === "claude")) return "/iamu:init";
29496
29613
  return "/iamu-setup (Claude Code: /iamu:init)";
29497
29614
  }
29498
- function detectTargetPort(projectRoot) {
29499
- if (process.env.PORT) {
29500
- const p = Number.parseInt(process.env.PORT, 10);
29501
- if (!Number.isNaN(p) && p > 0 && p <= 65535) return p;
29502
- }
29615
+ function detectDirPort(dir) {
29503
29616
  for (const envFile of [".env.local", ".env.development", ".env"]) {
29504
- const fullPath = join6(projectRoot, envFile);
29617
+ const fullPath = join6(dir, envFile);
29505
29618
  if (existsSync13(fullPath)) {
29506
29619
  try {
29507
29620
  const content = readFileSync6(fullPath, "utf-8");
@@ -29527,7 +29640,29 @@ function detectTargetPort(projectRoot) {
29527
29640
  }
29528
29641
  }
29529
29642
  }
29530
- const pkgPath = join6(projectRoot, "package.json");
29643
+ for (const f of [
29644
+ "vite.config.ts",
29645
+ "vite.config.js",
29646
+ "vite.config.mjs",
29647
+ "vite.config.cjs",
29648
+ "astro.config.mjs",
29649
+ "astro.config.ts",
29650
+ "astro.config.js"
29651
+ ]) {
29652
+ const cfgPath = join6(dir, f);
29653
+ if (existsSync13(cfgPath)) {
29654
+ try {
29655
+ const raw = readFileSync6(cfgPath, "utf-8");
29656
+ const portMatch = raw.match(/\bport\s*:\s*(\d{2,5})\b/);
29657
+ if (portMatch) {
29658
+ const p = Number.parseInt(portMatch[1], 10);
29659
+ if (!Number.isNaN(p) && p > 0 && p <= 65535) return p;
29660
+ }
29661
+ } catch {
29662
+ }
29663
+ }
29664
+ }
29665
+ const pkgPath = join6(dir, "package.json");
29531
29666
  if (existsSync13(pkgPath)) {
29532
29667
  try {
29533
29668
  const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
@@ -29546,11 +29681,12 @@ function detectTargetPort(projectRoot) {
29546
29681
  };
29547
29682
  if ("astro" in allDeps) return 4321;
29548
29683
  if ("vite" in allDeps) return 5173;
29684
+ if ("next" in allDeps) return 3e3;
29549
29685
  } catch {
29550
29686
  }
29551
29687
  }
29552
29688
  for (const f of ["astro.config.mjs", "astro.config.ts", "astro.config.js"]) {
29553
- if (existsSync13(join6(projectRoot, f))) return 4321;
29689
+ if (existsSync13(join6(dir, f))) return 4321;
29554
29690
  }
29555
29691
  for (const f of [
29556
29692
  "vite.config.ts",
@@ -29558,7 +29694,47 @@ function detectTargetPort(projectRoot) {
29558
29694
  "vite.config.mjs",
29559
29695
  "vite.config.cjs"
29560
29696
  ]) {
29561
- if (existsSync13(join6(projectRoot, f))) return 5173;
29697
+ if (existsSync13(join6(dir, f))) return 5173;
29698
+ }
29699
+ return void 0;
29700
+ }
29701
+ function detectTargetPort(projectRoot) {
29702
+ if (process.env.PORT) {
29703
+ const p = Number.parseInt(process.env.PORT, 10);
29704
+ if (!Number.isNaN(p) && p > 0 && p <= 65535) return p;
29705
+ }
29706
+ const rootPort = detectDirPort(projectRoot);
29707
+ if (rootPort !== void 0) return rootPort;
29708
+ const preferredSubdirs = [
29709
+ "apps/web",
29710
+ "apps/app",
29711
+ "apps/frontend",
29712
+ "apps/client",
29713
+ "packages/web",
29714
+ "packages/app",
29715
+ "packages/frontend",
29716
+ "web",
29717
+ "frontend",
29718
+ "client"
29719
+ ];
29720
+ for (const rel of preferredSubdirs) {
29721
+ const full = join6(projectRoot, rel);
29722
+ if (existsSync13(full)) {
29723
+ const p = detectDirPort(full);
29724
+ if (p !== void 0) return p;
29725
+ }
29726
+ }
29727
+ for (const parent of ["apps", "packages"]) {
29728
+ const parentDir = join6(projectRoot, parent);
29729
+ if (!existsSync13(parentDir)) continue;
29730
+ try {
29731
+ for (const entry of readdirSync3(parentDir, { withFileTypes: true })) {
29732
+ if (!entry.isDirectory()) continue;
29733
+ const p = detectDirPort(join6(parentDir, entry.name));
29734
+ if (p !== void 0) return p;
29735
+ }
29736
+ } catch {
29737
+ }
29562
29738
  }
29563
29739
  return void 0;
29564
29740
  }
@@ -29566,8 +29742,9 @@ function detectTargetUrl(projectRoot) {
29566
29742
  const port = detectTargetPort(projectRoot);
29567
29743
  return port ? `http://localhost:${port}` : "http://localhost:3000";
29568
29744
  }
29569
- function createDraftTestabilityContent(targetUrl = "http://localhost:3000") {
29745
+ function createDraftTestabilityContent(targetUrl = "http://localhost:3000", status2 = "draft") {
29570
29746
  const doc = emptyTestability(targetUrl);
29747
+ doc.frontmatter.status = status2;
29571
29748
  return serializeFrontmatter(doc.frontmatter, doc.body);
29572
29749
  }
29573
29750
  async function init(opts) {
@@ -29591,12 +29768,28 @@ async function init(opts) {
29591
29768
  if (existsSync13(join6(iamuDir2, "config.json"))) {
29592
29769
  const testabilityPath2 = join6(iamuDir2, "testability.md");
29593
29770
  if (!existsSync13(testabilityPath2)) {
29594
- const targetUrl = detectTargetUrl(projectRoot);
29771
+ const targetUrl = options.target || detectTargetUrl(projectRoot);
29595
29772
  await writeFile3(
29596
29773
  testabilityPath2,
29597
- createDraftTestabilityContent(targetUrl)
29774
+ createDraftTestabilityContent(
29775
+ targetUrl,
29776
+ options.target ? "verified" : "draft"
29777
+ )
29598
29778
  );
29599
29779
  backfilled.push("testability.md");
29780
+ } else if (options.target) {
29781
+ const existing = await readTestability(projectRoot);
29782
+ const fm = existing?.frontmatter ?? {
29783
+ schema_version: 1,
29784
+ status: "verified",
29785
+ target_url: options.target
29786
+ };
29787
+ fm.target_url = options.target;
29788
+ fm.status = "verified";
29789
+ await writeFile3(
29790
+ testabilityPath2,
29791
+ serializeFrontmatter(fm, existing?.body ?? "")
29792
+ );
29600
29793
  }
29601
29794
  const contextPath2 = join6(iamuDir2, "context.yml");
29602
29795
  if (!existsSync13(contextPath2)) {
@@ -29684,10 +29877,13 @@ async function init(opts) {
29684
29877
  );
29685
29878
  const testabilityPath2 = join6(iamuDir2, "testability.md");
29686
29879
  if (!existsSync13(testabilityPath2)) {
29687
- const targetUrl = detectTargetUrl(projectRoot);
29880
+ const targetUrl = options.target || detectTargetUrl(projectRoot);
29688
29881
  await writeFile3(
29689
29882
  testabilityPath2,
29690
- createDraftTestabilityContent(targetUrl)
29883
+ createDraftTestabilityContent(
29884
+ targetUrl,
29885
+ options.target ? "verified" : "draft"
29886
+ )
29691
29887
  );
29692
29888
  }
29693
29889
  }
@@ -29804,6 +30000,9 @@ async function init(opts) {
29804
30000
  }
29805
30001
  }
29806
30002
  }
30003
+ const testabilityDoc = await readTestability(projectRoot).catch(() => null);
30004
+ const configuredTarget = testabilityDoc?.frontmatter.target_url ?? detectTargetUrl(projectRoot);
30005
+ const targetStatus = testabilityDoc?.frontmatter.status ?? "draft";
29807
30006
  if (options.json) {
29808
30007
  console.log(
29809
30008
  JSON.stringify(
@@ -29813,6 +30012,8 @@ async function init(opts) {
29813
30012
  projectRoot,
29814
30013
  fresh: freshInit,
29815
30014
  backfilled,
30015
+ targetUrl: configuredTarget,
30016
+ targetStatus,
29816
30017
  tools: selectedHosts.map((h) => h.name),
29817
30018
  skillsCount: totalSkills,
29818
30019
  commandsCount: totalCommands,
@@ -29834,6 +30035,9 @@ async function init(opts) {
29834
30035
  )
29835
30036
  );
29836
30037
  console.log();
30038
+ console.log(
30039
+ ` ${chalk27.dim("Target:")} ${chalk27.cyan(configuredTarget)} ${chalk27.dim(targetStatus === "verified" ? "(verified)" : "(detected \u2014 change anytime with `iamu set testability --target <url>`)")}`
30040
+ );
29837
30041
  if (selectedHosts.length > 0) {
29838
30042
  const toolNames = selectedHosts.map((h) => h.name).join(", ");
29839
30043
  console.log(
@@ -29982,6 +30186,10 @@ async function status(opts) {
29982
30186
  const incompleteRuns = await listIncompleteRuns(projectRoot);
29983
30187
  const { rerun } = await computeFixStates(projectRoot);
29984
30188
  const staleSkills = await findStaleSkills(projectRoot);
30189
+ const testability = await readTestability(projectRoot).catch(() => null);
30190
+ const designState = await readDesignSources(projectRoot).catch(() => null);
30191
+ const designDocs = designState?.file.files.filter((f) => f.kind === "design") ?? [];
30192
+ const offerDesignTemplate = designDocs.length === 0 ? await shouldOfferTemplate(projectRoot).catch(() => false) : false;
29985
30193
  let exploreSummary = null;
29986
30194
  try {
29987
30195
  const maps = await listExploreMaps(projectRoot);
@@ -30007,6 +30215,21 @@ async function status(opts) {
30007
30215
  {
30008
30216
  project: projectStatus.project.name,
30009
30217
  mode: projectStatus.cloud.linked ? "cloud-linked" : "local-only",
30218
+ target: testability ? {
30219
+ url: testability.frontmatter.target_url,
30220
+ status: testability.frontmatter.status ?? "draft",
30221
+ dev_command: testability.frontmatter.dev_command ?? null,
30222
+ auth_strategy: testability.frontmatter.auth_strategy ?? null,
30223
+ access: testability.frontmatter.access ?? null
30224
+ } : null,
30225
+ design: {
30226
+ recorded: designDocs.map((f) => f.path),
30227
+ sources: designState?.file.files.map((f) => ({
30228
+ path: f.path,
30229
+ kind: f.kind
30230
+ })) ?? [],
30231
+ offer_template: offerDesignTemplate
30232
+ },
30010
30233
  entities: Object.fromEntries(
30011
30234
  Object.entries(projectStatus.entities).map(([k, v]) => [
30012
30235
  k,
@@ -30092,6 +30315,18 @@ async function status(opts) {
30092
30315
  console.log(` ${padRight(label, 14)} ${num2}${inv}`);
30093
30316
  }
30094
30317
  if (!quiet) console.log();
30318
+ if (testability?.frontmatter.target_url) {
30319
+ const tfm = testability.frontmatter;
30320
+ const isVerified = tfm.status === "verified";
30321
+ const meta = [
30322
+ isVerified ? "verified" : "unverified",
30323
+ tfm.auth_strategy ? `auth: ${tfm.auth_strategy}` : null,
30324
+ tfm.dev_command ? `dev: ${tfm.dev_command}` : null
30325
+ ].filter(Boolean).join(" \xB7 ");
30326
+ console.log(
30327
+ ` ${padRight("target", 14)} ${chalk28.cyan(tfm.target_url)}${chalk28.dim(` (${meta})`)}${!isVerified ? chalk28.dim(" \u2014 `iamu set testability --target <url>`") : ""}`
30328
+ );
30329
+ }
30095
30330
  let ctxLabel = chalk28.dim("none");
30096
30331
  if (projectStatus.context.present) {
30097
30332
  if (projectStatus.context.richness === "thin") {
@@ -30102,6 +30337,19 @@ async function status(opts) {
30102
30337
  }
30103
30338
  }
30104
30339
  console.log(` ${padRight("context", 14)} ${ctxLabel}`);
30340
+ if (designDocs.length > 0) {
30341
+ console.log(
30342
+ ` ${padRight("design", 14)} ${chalk28.green(designDocs.map((f) => f.path).join(", "))}`
30343
+ );
30344
+ } else if (offerDesignTemplate) {
30345
+ console.log(
30346
+ ` ${padRight("design", 14)} ${chalk28.yellow("none")}${chalk28.dim(" (general craft only \u2014 run `iamu-design` so runs check your own design rules)")}`
30347
+ );
30348
+ } else {
30349
+ console.log(
30350
+ ` ${padRight("design", 14)} ${chalk28.dim("none (general craft)")}`
30351
+ );
30352
+ }
30105
30353
  console.log(
30106
30354
  ` ${padRight("runs", 14)} ${runs.length === 0 ? chalk28.dim("0") : chalk28.cyan(String(runs.length))}${runs.length > 0 ? chalk28.dim(" last: " + runs.at(-1)) : ""}`
30107
30355
  );
@@ -30441,7 +30689,8 @@ async function fillOrSelect(el, text, submit) {
30441
30689
  if (submit) await el.press("Enter");
30442
30690
  }
30443
30691
  async function markAndQueryIndexedElements(page) {
30444
- return page.evaluate((selector) => {
30692
+ if (typeof page.evaluate !== "function") return [];
30693
+ const evaluated = await page.evaluate((selector) => {
30445
30694
  const g = globalThis;
30446
30695
  function collectElements(root) {
30447
30696
  const result = [];
@@ -30466,6 +30715,41 @@ async function markAndQueryIndexedElements(page) {
30466
30715
  }
30467
30716
  const elements = collectElements(g.document);
30468
30717
  const results = [];
30718
+ function normalizeText(raw) {
30719
+ return (raw || "").trim().replace(/\s+/g, " ").slice(0, 80);
30720
+ }
30721
+ function getAssociatedLabel(el) {
30722
+ const labelledBy = el.getAttribute("aria-labelledby");
30723
+ if (labelledBy && typeof g.document.getElementById === "function") {
30724
+ const parts = [];
30725
+ for (const id of labelledBy.split(/\s+/)) {
30726
+ if (!id) continue;
30727
+ const node = g.document.getElementById(id);
30728
+ const txt = normalizeText(node?.textContent);
30729
+ if (txt) parts.push(txt);
30730
+ }
30731
+ if (parts.length > 0) return normalizeText(parts.join(" "));
30732
+ }
30733
+ if (el.labels && el.labels.length > 0) {
30734
+ const parts = [];
30735
+ for (let i = 0; i < el.labels.length; i++) {
30736
+ const lbl = el.labels[i];
30737
+ const txt = normalizeText(lbl?.innerText ?? lbl?.textContent);
30738
+ if (txt) parts.push(txt);
30739
+ }
30740
+ if (parts.length > 0) return normalizeText(parts.join(" "));
30741
+ }
30742
+ if (typeof el.closest === "function") {
30743
+ const parentLabel = el.closest("label");
30744
+ if (parentLabel) {
30745
+ const txt = normalizeText(
30746
+ parentLabel.innerText ?? parentLabel.textContent
30747
+ );
30748
+ if (txt) return txt;
30749
+ }
30750
+ }
30751
+ return "";
30752
+ }
30469
30753
  let count = 0;
30470
30754
  for (const el of elements) {
30471
30755
  const style = g.getComputedStyle(el);
@@ -30480,7 +30764,10 @@ async function markAndQueryIndexedElements(page) {
30480
30764
  const roleAttr = el.getAttribute("role");
30481
30765
  const type2 = tag === "input" ? el.type?.toLowerCase() || "text" : void 0;
30482
30766
  const disabled = Boolean(el.disabled) || el.getAttribute("aria-disabled") === "true";
30483
- const name = el.getAttribute("aria-label") || el.getAttribute("title") || el.placeholder || (tag === "input" || tag === "textarea" ? el.value : "") || (el.textContent || "").trim().slice(0, 80).replace(/\s+/g, " ") || "";
30767
+ const isCheckable = type2 === "radio" || type2 === "checkbox" || roleAttr === "radio" || roleAttr === "checkbox" || roleAttr === "switch";
30768
+ const checked = isCheckable ? Boolean(el.checked) || el.getAttribute("aria-checked") === "true" : void 0;
30769
+ const labelText = tag === "input" || tag === "select" || tag === "textarea" ? getAssociatedLabel(el) : "";
30770
+ const name = normalizeText(el.getAttribute("aria-label")) || labelText || normalizeText(el.getAttribute("title")) || normalizeText(el.placeholder) || (tag === "input" || tag === "textarea" ? normalizeText(el.value) : "") || normalizeText(el.textContent) || "";
30484
30771
  const role = roleAttr || (tag === "button" ? "button" : tag === "a" ? "link" : tag === "input" ? type2 || "textbox" : tag === "select" ? "combobox" : tag);
30485
30772
  results.push({
30486
30773
  index: count,
@@ -30488,11 +30775,38 @@ async function markAndQueryIndexedElements(page) {
30488
30775
  role,
30489
30776
  name,
30490
30777
  ...type2 ? { type: type2 } : {},
30491
- ...disabled ? { disabled: true } : {}
30778
+ ...disabled ? { disabled: true } : {},
30779
+ ...checked ? { checked: true } : {}
30492
30780
  });
30493
30781
  }
30494
30782
  return results;
30495
30783
  }, INDEXED_SELECTOR);
30784
+ return Array.isArray(evaluated) ? evaluated : [];
30785
+ }
30786
+ async function clickLocatorWithFallback(loc, timeoutMs = ACTION_TIMEOUT_MS) {
30787
+ if (typeof loc.count !== "function") {
30788
+ await loc.click({ timeout: timeoutMs });
30789
+ return;
30790
+ }
30791
+ const presentCount = await loc.count().catch(() => 0);
30792
+ if (presentCount > 0) {
30793
+ try {
30794
+ await loc.click({ timeout: 250 });
30795
+ return;
30796
+ } catch (err) {
30797
+ if (typeof loc.evaluate === "function") {
30798
+ await loc.evaluate((node) => {
30799
+ const el = node;
30800
+ if (typeof el.click === "function") {
30801
+ el.click();
30802
+ }
30803
+ });
30804
+ return;
30805
+ }
30806
+ throw err;
30807
+ }
30808
+ }
30809
+ await loc.click({ timeout: Math.min(timeoutMs, 1500) });
30496
30810
  }
30497
30811
  async function clickIndexedElement(page, index) {
30498
30812
  let loc = page.locator(`[data-iamu-index="${index}"]`);
@@ -30505,7 +30819,19 @@ async function clickIndexedElement(page, index) {
30505
30819
  `Nothing on the page matches interactive element [#${index}]. Run \`iamu browse snapshot --indexed\` to view available elements.`
30506
30820
  );
30507
30821
  }
30508
- await loc.first().click({ timeout: ACTION_TIMEOUT_MS });
30822
+ const first = loc.first();
30823
+ try {
30824
+ await first.click({ timeout: 250 });
30825
+ } catch (err) {
30826
+ if (typeof first.evaluate === "function") {
30827
+ await first.evaluate((node) => {
30828
+ const el = node;
30829
+ if (typeof el.click === "function") el.click();
30830
+ });
30831
+ return;
30832
+ }
30833
+ throw err;
30834
+ }
30509
30835
  }
30510
30836
  async function fillIndexedElement(page, index, text, submit) {
30511
30837
  let loc = page.locator(`[data-iamu-index="${index}"]`);
@@ -30639,13 +30965,24 @@ var init_browse_actions = __esm({
30639
30965
  usage: 'click <role> "<name>" [--nth N]',
30640
30966
  minArgs: 2,
30641
30967
  target: roleTarget,
30642
- run: (p, args2, opts) => byRole(p, args2[0], args2[1], opts).click({ timeout: ACTION_TIMEOUT_MS })
30968
+ run: async (p, args2, opts) => {
30969
+ const [role, name] = args2;
30970
+ const loc = byRole(p, role, name, opts);
30971
+ if ((role === "radio" || role === "checkbox") && typeof loc.count === "function" && await loc.count().catch(() => 1) === 0 && typeof p.locator === "function") {
30972
+ const byValue = p.locator(`input[type="${role}"][value=${JSON.stringify(name)}]`).first();
30973
+ if (await byValue.count().catch(() => 0) > 0) {
30974
+ await clickLocatorWithFallback(byValue, 1500);
30975
+ return;
30976
+ }
30977
+ }
30978
+ await clickLocatorWithFallback(loc, ACTION_TIMEOUT_MS);
30979
+ }
30643
30980
  },
30644
30981
  "click-text": {
30645
30982
  usage: 'click-text "<visible text>"',
30646
30983
  minArgs: 1,
30647
30984
  target: (args2) => `the text \u201C${args2[0]}\u201D`,
30648
- run: (p, args2) => p.getByText(args2[0]).first().click({ timeout: ACTION_TIMEOUT_MS })
30985
+ run: (p, args2) => clickLocatorWithFallback(p.getByText(args2[0]).first(), ACTION_TIMEOUT_MS)
30649
30986
  },
30650
30987
  fill: fillAction("fill"),
30651
30988
  select: fillAction("select"),
@@ -30740,7 +31077,7 @@ var init_browse_actions = __esm({
30740
31077
  });
30741
31078
 
30742
31079
  // src/commands/browse-driver.ts
30743
- import { execFile as execFile3, spawn } from "child_process";
31080
+ import { execFile as execFile3, execFileSync, spawn } from "child_process";
30744
31081
  import { readFile as readFile5 } from "fs/promises";
30745
31082
  async function probeCdp(port, timeoutMs = CDP_PROBE_TIMEOUT_MS) {
30746
31083
  try {
@@ -30755,7 +31092,29 @@ async function probeCdp(port, timeoutMs = CDP_PROBE_TIMEOUT_MS) {
30755
31092
  }
30756
31093
  }
30757
31094
  async function readProcessCommandLine(pid, platform2 = process.platform) {
30758
- if (!Number.isInteger(pid) || pid <= 0 || platform2 === "win32") return null;
31095
+ if (!Number.isInteger(pid) || pid <= 0) return null;
31096
+ if (platform2 === "win32") {
31097
+ try {
31098
+ const out2 = execFileSync(
31099
+ "powershell.exe",
31100
+ [
31101
+ "-NoProfile",
31102
+ "-NonInteractive",
31103
+ "-Command",
31104
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
31105
+ ],
31106
+ {
31107
+ encoding: "utf-8",
31108
+ stdio: ["ignore", "pipe", "ignore"],
31109
+ timeout: 3e3,
31110
+ windowsHide: true
31111
+ }
31112
+ );
31113
+ return out2.trim() || null;
31114
+ } catch {
31115
+ return null;
31116
+ }
31117
+ }
30759
31118
  if (platform2 === "linux") {
30760
31119
  const raw = await readFile5(`/proc/${pid}/cmdline`, "utf8").catch(() => "");
30761
31120
  if (raw) return raw.split("\0").join(" ").trim();
@@ -30770,13 +31129,32 @@ async function readProcessCommandLine(pid, platform2 = process.platform) {
30770
31129
  });
30771
31130
  }
30772
31131
  function isOurBrowser(commandLine, profileDir2) {
30773
- if (!commandLine) return false;
30774
- const flag = `--user-data-dir=${profileDir2}`;
30775
- for (let i = commandLine.indexOf(flag); i !== -1; ) {
30776
- const next = commandLine[i + flag.length];
30777
- if (next === void 0 || next === " ") return true;
30778
- i = commandLine.indexOf(flag, i + 1);
30779
- }
31132
+ if (!commandLine || !BROWSER_NAME_RE.test(commandLine)) return false;
31133
+ if (!profileDir2) return false;
31134
+ const matches = (cmd, dir) => {
31135
+ const flags = [
31136
+ `--user-data-dir=${dir}`,
31137
+ `--user-data-dir="${dir}"`,
31138
+ `--user-data-dir='${dir}'`,
31139
+ `--user-data-dir="${dir}`,
31140
+ `--user-data-dir='${dir}`
31141
+ ];
31142
+ for (const flag of flags) {
31143
+ for (let i = cmd.indexOf(flag); i !== -1; ) {
31144
+ const next = cmd[i + flag.length];
31145
+ if (next === void 0 || next === " " || next === " " || next === "\r" || next === "\n" || next === `"` || next === `'`) {
31146
+ return true;
31147
+ }
31148
+ i = cmd.indexOf(flag, i + 1);
31149
+ }
31150
+ }
31151
+ return false;
31152
+ };
31153
+ if (matches(commandLine, profileDir2)) return true;
31154
+ const normCmd = commandLine.replace(/\\/g, "/");
31155
+ const normDir = profileDir2.replace(/\\/g, "/");
31156
+ if (matches(normCmd, normDir)) return true;
31157
+ if (matches(normCmd.toLowerCase(), normDir.toLowerCase())) return true;
30780
31158
  return false;
30781
31159
  }
30782
31160
  async function reapBrowser(pid, profileDir2, driver) {
@@ -30785,15 +31163,28 @@ async function reapBrowser(pid, profileDir2, driver) {
30785
31163
  driver.kill(pid);
30786
31164
  return true;
30787
31165
  }
31166
+ function selectTargetPage(rawPages, pageIndex) {
31167
+ const nonDevtools = rawPages.filter((p) => !p.url().startsWith("devtools:"));
31168
+ const userPages = nonDevtools.filter(
31169
+ (p) => !/^(?:chrome-extension|chrome|edge):/i.test(p.url())
31170
+ );
31171
+ const pages = userPages.length > 0 ? userPages : nonDevtools;
31172
+ if (pageIndex !== void 0 && pages[pageIndex]) {
31173
+ return { page: pages[pageIndex], pages };
31174
+ }
31175
+ const preferred = pages.filter((p) => p.url() && p.url() !== "about:blank").at(-1);
31176
+ return { page: preferred ?? pages.at(-1), pages };
31177
+ }
30788
31178
  async function connect(port) {
30789
31179
  const { chromium } = await import("playwright-core");
30790
31180
  return chromium.connectOverCDP(`http://127.0.0.1:${port}`);
30791
31181
  }
30792
- var CDP_PROBE_TIMEOUT_MS, cdpDriver;
31182
+ var CDP_PROBE_TIMEOUT_MS, BROWSER_NAME_RE, cdpDriver;
30793
31183
  var init_browse_driver = __esm({
30794
31184
  "src/commands/browse-driver.ts"() {
30795
31185
  "use strict";
30796
31186
  CDP_PROBE_TIMEOUT_MS = 1e3;
31187
+ BROWSER_NAME_RE = /(?:chrome|chromium|brave|msedge)/i;
30797
31188
  cdpDriver = {
30798
31189
  isRunning: (port) => probeCdp(port),
30799
31190
  launch(chrome, args2) {
@@ -30834,9 +31225,11 @@ var init_browse_driver = __esm({
30834
31225
  });
30835
31226
  try {
30836
31227
  const context = browser.contexts()[0] ?? await browser.newContext();
30837
- const pages = context.pages().filter((p) => !p.url().startsWith("devtools:"));
30838
- const targetPage = pageIndex !== void 0 && pages[pageIndex] || pages.at(-1) || await context.newPage();
30839
- const page = targetPage;
31228
+ const { page: targetPage, pages } = selectTargetPage(
31229
+ context.pages(),
31230
+ pageIndex
31231
+ );
31232
+ const page = targetPage ?? await context.newPage();
30840
31233
  const dialogs = [];
30841
31234
  page.on("dialog", (dialog) => {
30842
31235
  const type2 = dialog.type();
@@ -30869,15 +31262,28 @@ var init_browse_driver = __esm({
30869
31262
  }
30870
31263
  },
30871
31264
  commandLine: (pid) => readProcessCommandLine(pid),
30872
- kill(pid) {
31265
+ kill(pid, signal = "SIGTERM") {
30873
31266
  try {
30874
- if (process.platform !== "win32") {
30875
- try {
30876
- process.kill(-pid, "SIGTERM");
30877
- } catch {
31267
+ if (process.platform === "win32") {
31268
+ if (signal === "SIGKILL") {
31269
+ try {
31270
+ execFileSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
31271
+ stdio: "ignore",
31272
+ timeout: 3e3,
31273
+ windowsHide: true
31274
+ });
31275
+ return;
31276
+ } catch {
31277
+ }
30878
31278
  }
31279
+ process.kill(pid, signal);
31280
+ return;
31281
+ }
31282
+ try {
31283
+ process.kill(-pid, signal);
31284
+ } catch {
30879
31285
  }
30880
- process.kill(pid, "SIGTERM");
31286
+ process.kill(pid, signal);
30881
31287
  } catch {
30882
31288
  }
30883
31289
  }
@@ -30940,7 +31346,7 @@ __export(step_exports, {
30940
31346
  recordStep: () => recordStep,
30941
31347
  stepCommand: () => stepCommand
30942
31348
  });
30943
- import { copyFile, mkdir as mkdir5, stat as stat2 } from "fs/promises";
31349
+ import { copyFile, mkdir as mkdir5, rename, stat as stat2 } from "fs/promises";
30944
31350
  import path56 from "path";
30945
31351
  import chalk29 from "chalk";
30946
31352
  function parseMaxSteps(val) {
@@ -30975,11 +31381,18 @@ async function recordStep(input) {
30975
31381
  let finalScreenshotPath = null;
30976
31382
  if (shotSource) {
30977
31383
  const destName = `step-${stepNumber2}.png`;
30978
- finalScreenshotPath = path56.join(screenshotsDir, destName);
30979
- if (path56.resolve(finalScreenshotPath) !== shotSource) {
30980
- await copyFile(shotSource, finalScreenshotPath);
31384
+ const destPath = path56.join(screenshotsDir, destName);
31385
+ finalScreenshotPath = destPath;
31386
+ if (path56.resolve(destPath) !== shotSource) {
31387
+ if (input.moveShot) {
31388
+ await rename(shotSource, destPath).catch(
31389
+ () => copyFile(shotSource, destPath)
31390
+ );
31391
+ } else {
31392
+ await copyFile(shotSource, destPath);
31393
+ }
30981
31394
  }
30982
- screenshotRef = path56.join("screenshots", destName);
31395
+ screenshotRef = path56.posix.join("screenshots", destName);
30983
31396
  }
30984
31397
  const parsedStep = runStepSchema.parse({
30985
31398
  step_number: stepNumber2,
@@ -31072,7 +31485,7 @@ async function stepCommand(runArg, opts) {
31072
31485
  {
31073
31486
  step_number: res.step.step_number,
31074
31487
  cancelled: true,
31075
- path: path56.relative(projectRoot, res.stepPath),
31488
+ path: path56.relative(projectRoot, res.stepPath).split(path56.sep).join("/"),
31076
31489
  action: res.step.action,
31077
31490
  note: res.step.note,
31078
31491
  url: res.step.url,
@@ -31100,7 +31513,7 @@ async function stepCommand(runArg, opts) {
31100
31513
  JSON.stringify(
31101
31514
  {
31102
31515
  step_number: res.step.step_number,
31103
- path: path56.relative(projectRoot, res.stepPath),
31516
+ path: path56.relative(projectRoot, res.stepPath).split(path56.sep).join("/"),
31104
31517
  action: res.step.action,
31105
31518
  note: res.step.note,
31106
31519
  url: res.step.url,
@@ -31174,8 +31587,10 @@ __export(browse_exports, {
31174
31587
  detectAntiBotChallenge: () => detectAntiBotChallenge,
31175
31588
  findChrome: () => findChrome,
31176
31589
  isHeadlessEnvironment: () => isHeadlessEnvironment,
31590
+ parseActionCommand: () => parseActionCommand,
31177
31591
  parseViewport: () => parseViewport,
31178
31592
  readBrowseState: () => readBrowseState,
31593
+ resilientAriaSnapshot: () => resilientAriaSnapshot,
31179
31594
  shouldDisableSandbox: () => shouldDisableSandbox,
31180
31595
  start: () => start,
31181
31596
  waitForVisualStability: () => waitForVisualStability,
@@ -31240,7 +31655,10 @@ function findChrome(env = process.env, platform2 = process.platform, exists = ex
31240
31655
  ] : platform2 === "win32" ? [
31241
31656
  `${env.PROGRAMFILES ?? "C:\\Program Files"}\\Google\\Chrome\\Application\\chrome.exe`,
31242
31657
  `${env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)"}\\Google\\Chrome\\Application\\chrome.exe`,
31243
- `${env.LOCALAPPDATA ?? ""}\\Google\\Chrome\\Application\\chrome.exe`
31658
+ `${env.LOCALAPPDATA ?? ""}\\Google\\Chrome\\Application\\chrome.exe`,
31659
+ `${env.PROGRAMFILES ?? "C:\\Program Files"}\\Microsoft\\Edge\\Application\\msedge.exe`,
31660
+ `${env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)"}\\Microsoft\\Edge\\Application\\msedge.exe`,
31661
+ `${env.LOCALAPPDATA ?? ""}\\Microsoft\\Edge\\Application\\msedge.exe`
31244
31662
  ] : [
31245
31663
  "/usr/bin/google-chrome",
31246
31664
  "/usr/bin/google-chrome-stable",
@@ -31429,7 +31847,7 @@ function checkAndRecordStagnation(projectRoot, runId, fingerprint, action) {
31429
31847
  }
31430
31848
  return { count };
31431
31849
  }
31432
- async function waitForVisualStability(page, maxWaitMs = 300) {
31850
+ async function waitForVisualStability(page, maxWaitMs = 150) {
31433
31851
  let timer;
31434
31852
  try {
31435
31853
  const nodeTimeout = new Promise((resolve3) => {
@@ -31531,7 +31949,7 @@ async function detectAntiBotChallenge(page, allowEvaluate = true) {
31531
31949
  }
31532
31950
  return void 0;
31533
31951
  }
31534
- async function withPage(opts, driver, fn, target) {
31952
+ async function withPage(opts, driver, fn, target, onPostShot) {
31535
31953
  const state = readBrowseState();
31536
31954
  if (!state) {
31537
31955
  fail(NO_BROWSER, opts);
@@ -31541,23 +31959,53 @@ async function withPage(opts, driver, fn, target) {
31541
31959
  try {
31542
31960
  attached = state.activeTabIndex !== void 0 ? await driver.attach(state.port, state.activeTabIndex) : await driver.attach(state.port);
31543
31961
  } catch {
31544
- fail(NO_BROWSER, opts);
31545
- return null;
31962
+ const chrome = findChrome();
31963
+ if (!chrome) {
31964
+ fail(NO_BROWSER, opts);
31965
+ return null;
31966
+ }
31967
+ try {
31968
+ if (state.pid) {
31969
+ await reapBrowser(state.pid, profileDir(), driver);
31970
+ }
31971
+ const headless = state.headless ?? isHeadlessEnvironment(opts);
31972
+ const pid = driver.launch(chrome, chromeArgs(opts, state.port, headless));
31973
+ let running = false;
31974
+ for (let i = 0; i < 25; i++) {
31975
+ if (await driver.isRunning(state.port)) {
31976
+ running = true;
31977
+ break;
31978
+ }
31979
+ await new Promise((r) => setTimeout(r, 200));
31980
+ }
31981
+ if (!running) {
31982
+ fail(NO_BROWSER, opts);
31983
+ return null;
31984
+ }
31985
+ writeState({ ...state, pid, headless });
31986
+ attached = state.activeTabIndex !== void 0 ? await driver.attach(state.port, state.activeTabIndex) : await driver.attach(state.port);
31987
+ } catch {
31988
+ fail(NO_BROWSER, opts);
31989
+ return null;
31990
+ }
31546
31991
  }
31547
31992
  const { page } = attached;
31548
31993
  try {
31549
31994
  if (state.viewport) {
31550
- await page.setViewportSize(state.viewport);
31551
- if (state.viewport.width <= 500 && typeof page.context === "function") {
31552
- try {
31553
- const context = page.context();
31554
- if (context && typeof context.newCDPSession === "function") {
31555
- const cdp = await context.newCDPSession(page);
31556
- await cdp.send("Emulation.setTouchEmulationEnabled", {
31557
- enabled: true
31558
- });
31995
+ const currentVp = typeof page.viewportSize === "function" ? page.viewportSize() : null;
31996
+ if (!currentVp || currentVp.width !== state.viewport.width || currentVp.height !== state.viewport.height) {
31997
+ await page.setViewportSize(state.viewport);
31998
+ if (state.viewport.width <= 500 && typeof page.context === "function") {
31999
+ try {
32000
+ const context = page.context();
32001
+ if (context && typeof context.newCDPSession === "function") {
32002
+ const cdp = await context.newCDPSession(page);
32003
+ await cdp.send("Emulation.setTouchEmulationEnabled", {
32004
+ enabled: true
32005
+ });
32006
+ }
32007
+ } catch {
31559
32008
  }
31560
- } catch {
31561
32009
  }
31562
32010
  }
31563
32011
  }
@@ -31582,9 +32030,12 @@ async function withPage(opts, driver, fn, target) {
31582
32030
  }).catch(() => void 0);
31583
32031
  }
31584
32032
  }
32033
+ if (onPostShot) {
32034
+ await onPostShot(page);
32035
+ }
31585
32036
  const dialogs = attached.dialogs ?? [];
31586
32037
  const dialog = dialogs.length > 0 ? dialogs.join("; ") : void 0;
31587
- const challenge = await detectAntiBotChallenge(page, Boolean(opts.shot));
32038
+ const challenge = opts.shot ? await detectAntiBotChallenge(page, true) : void 0;
31588
32039
  return {
31589
32040
  url: page.url(),
31590
32041
  title: await page.title(),
@@ -31624,70 +32075,89 @@ function reportPage(opts, result, extra = {}) {
31624
32075
  human
31625
32076
  );
31626
32077
  }
32078
+ async function resilientAriaSnapshot(page) {
32079
+ try {
32080
+ return await page.locator("body").ariaSnapshot({ timeout: 5e3 }) ?? "";
32081
+ } catch (originalErr) {
32082
+ try {
32083
+ return await page.locator(":root").ariaSnapshot({ timeout: 3e3 }) ?? "";
32084
+ } catch {
32085
+ if (page.url() === "about:blank") {
32086
+ return "";
32087
+ }
32088
+ throw originalErr;
32089
+ }
32090
+ }
32091
+ }
32092
+ function formatIndexedElementLine(el) {
32093
+ return ` [#${el.index}] ${el.role}${el.name ? ` "${el.name}"` : ""}${el.checked ? " [checked]" : ""}${el.disabled ? " [disabled]" : ""}`;
32094
+ }
32095
+ async function captureSnapshotWithFrames(p, includeIndexed) {
32096
+ let snapshot = await resilientAriaSnapshot(p);
32097
+ if (typeof p.frames === "function") {
32098
+ const childFrames = p.frames().filter((f) => {
32099
+ try {
32100
+ return f !== (typeof p.mainFrame === "function" ? p.mainFrame() : null);
32101
+ } catch {
32102
+ return false;
32103
+ }
32104
+ });
32105
+ if (childFrames.length > 0) {
32106
+ const frameSnapshots = [];
32107
+ for (let i = 0; i < childFrames.length; i++) {
32108
+ const f = childFrames[i];
32109
+ const frameUrl = typeof f.url === "function" ? f.url() : "";
32110
+ if (!frameUrl || frameUrl === "about:blank") continue;
32111
+ const frameName = (typeof f.name === "function" ? f.name() : "") || `frame-${i + 1}`;
32112
+ try {
32113
+ const snap = await f.locator("body").ariaSnapshot({ timeout: 1500 });
32114
+ if (snap && snap.trim()) {
32115
+ frameSnapshots.push(
32116
+ `[Iframe: ${frameName} (${frameUrl})]
32117
+ ${snap.trim()}`
32118
+ );
32119
+ }
32120
+ } catch {
32121
+ }
32122
+ }
32123
+ if (frameSnapshots.length > 0) {
32124
+ snapshot = `${snapshot}
32125
+
32126
+ ${frameSnapshots.join("\n\n")}`;
32127
+ }
32128
+ }
32129
+ }
32130
+ let elements = [];
32131
+ if (includeIndexed) {
32132
+ elements = await markAndQueryIndexedElements(p);
32133
+ }
32134
+ const formatted = includeIndexed ? [
32135
+ "Interactive elements:",
32136
+ ...elements.map(formatIndexedElementLine),
32137
+ "",
32138
+ "Accessibility snapshot:",
32139
+ snapshot
32140
+ ].join("\n") : snapshot;
32141
+ return { snapshot, elements, formatted };
32142
+ }
31627
32143
  async function pageCommand(command, args2, opts, driver) {
31628
32144
  if (command === "snapshot") {
31629
- let snapshot = "";
31630
- let elements = [];
32145
+ let captured;
31631
32146
  const result2 = await withPage(
31632
32147
  { ...opts, shot: void 0 },
31633
32148
  driver,
31634
32149
  async (p) => {
31635
- snapshot = await p.locator("body").ariaSnapshot();
31636
- if (typeof p.frames === "function") {
31637
- const childFrames = p.frames().filter((f) => {
31638
- try {
31639
- return f !== (typeof p.mainFrame === "function" ? p.mainFrame() : null);
31640
- } catch {
31641
- return false;
31642
- }
31643
- });
31644
- if (childFrames.length > 0) {
31645
- const frameSnapshots = [];
31646
- for (let i = 0; i < childFrames.length; i++) {
31647
- const f = childFrames[i];
31648
- const frameUrl = typeof f.url === "function" ? f.url() : "";
31649
- if (!frameUrl || frameUrl === "about:blank") continue;
31650
- const frameName = (typeof f.name === "function" ? f.name() : "") || `frame-${i + 1}`;
31651
- try {
31652
- const snap = await f.locator("body").ariaSnapshot({ timeout: 1500 });
31653
- if (snap && snap.trim()) {
31654
- frameSnapshots.push(
31655
- `[Iframe: ${frameName} (${frameUrl})]
31656
- ${snap.trim()}`
31657
- );
31658
- }
31659
- } catch {
31660
- }
31661
- }
31662
- if (frameSnapshots.length > 0) {
31663
- snapshot = `${snapshot}
31664
-
31665
- ${frameSnapshots.join("\n\n")}`;
31666
- }
31667
- }
31668
- }
31669
- if (opts.indexed) {
31670
- elements = await markAndQueryIndexedElements(p);
31671
- }
32150
+ captured = await captureSnapshotWithFrames(p, Boolean(opts.indexed));
31672
32151
  }
31673
32152
  );
31674
- if (result2) {
32153
+ if (result2 && captured) {
31675
32154
  if (opts.indexed) {
31676
- const lines = [
31677
- "Interactive elements:",
31678
- ...elements.map(
31679
- (el) => ` [#${el.index}] ${el.role}${el.name ? ` "${el.name}"` : ""}${el.disabled ? " [disabled]" : ""}`
31680
- ),
31681
- "",
31682
- "Accessibility snapshot:",
31683
- snapshot
31684
- ];
31685
32155
  reportPage(opts, result2, {
31686
- snapshot: lines.join("\n"),
31687
- interactive_elements: elements
32156
+ snapshot: captured.formatted,
32157
+ interactive_elements: captured.elements
31688
32158
  });
31689
32159
  } else {
31690
- reportPage(opts, result2, { snapshot });
32160
+ reportPage(opts, result2, { snapshot: captured.snapshot });
31691
32161
  }
31692
32162
  }
31693
32163
  return;
@@ -31721,30 +32191,54 @@ ${frameSnapshots.join("\n\n")}`;
31721
32191
  if (args2.length < action.minArgs) {
31722
32192
  return fail(`Usage: iamu browse ${action.usage}`, opts);
31723
32193
  }
32194
+ let gotoSnapshot;
31724
32195
  const result = await withPage(
31725
32196
  opts,
31726
32197
  driver,
31727
32198
  (p) => action.run(p, args2, opts),
31728
- action.target?.(args2)
32199
+ action.target?.(args2),
32200
+ command === "goto" && opts.snapshot !== false ? async (p) => {
32201
+ if (typeof p.locator === "function") {
32202
+ gotoSnapshot = await captureSnapshotWithFrames(p, true).catch(
32203
+ () => void 0
32204
+ );
32205
+ }
32206
+ } : void 0
31729
32207
  );
31730
- if (result) reportPage(opts, result);
32208
+ if (result) {
32209
+ const snap = gotoSnapshot;
32210
+ const hasSnapshot = snap && (snap.snapshot.length > 0 || snap.elements.length > 0);
32211
+ reportPage(
32212
+ opts,
32213
+ result,
32214
+ hasSnapshot ? {
32215
+ snapshot: snap.formatted,
32216
+ interactive_elements: snap.elements
32217
+ } : {}
32218
+ );
32219
+ }
31731
32220
  }
31732
- function printStep(opts, record, projectRoot, stagnationWarning, challengeWarning) {
32221
+ function printStep(opts, record, projectRoot, stagnationWarning, challengeWarning, stepSnapshot) {
31733
32222
  const { step } = record;
31734
32223
  const warnings = [challengeWarning, stagnationWarning].filter(
31735
32224
  Boolean
31736
32225
  );
32226
+ const hasSnapshot = stepSnapshot && (stepSnapshot.snapshot.length > 0 || stepSnapshot.elements.length > 0);
31737
32227
  if (opts.json) {
31738
32228
  const doc = {
31739
32229
  step_number: step.step_number,
31740
- path: path57.relative(projectRoot, record.stepPath),
32230
+ path: path57.relative(projectRoot, record.stepPath).split(path57.sep).join("/"),
31741
32231
  action: step.action,
31742
32232
  note: step.note,
31743
32233
  url: step.url,
31744
32234
  screenshot: step.screenshot,
31745
32235
  surface: step.surface,
31746
32236
  ...warnings.length > 0 ? { warning: warnings.join(" | ") } : {},
31747
- ...record.cancelled ? { cancelled: true } : {}
32237
+ ...record.cancelled ? { cancelled: true } : {},
32238
+ ...hasSnapshot ? {
32239
+ snapshot: stepSnapshot.formatted,
32240
+ interactive_elements: stepSnapshot.elements
32241
+ } : {}
31748
32242
  };
31749
32243
  console.log(JSON.stringify(doc, null, 2));
31750
32244
  return;
@@ -31778,6 +32272,9 @@ function printStep(opts, record, projectRoot, stagnationWarning, challengeWarnin
31778
32272
  console.log(` ${symbols.warning} ${chalk30.yellow(w)}`);
31779
32273
  }
31780
32274
  }
32275
+ if (hasSnapshot) {
32276
+ console.log(stepSnapshot.formatted);
32277
+ }
31781
32278
  }
31782
32279
  async function clearOriginStorage(origin, driver = cdpDriver, returnTo) {
31783
32280
  const state = readBrowseState();
@@ -31868,6 +32365,45 @@ async function clearStorage(opts, driver) {
31868
32365
  ` opened ${returnTo}`
31869
32366
  ]);
31870
32367
  }
32368
+ function parseActionCommand(raw) {
32369
+ const tokens = [];
32370
+ let current = "";
32371
+ let quote = null;
32372
+ let hasToken = false;
32373
+ for (let i = 0; i < raw.length; i++) {
32374
+ const ch = raw[i];
32375
+ if (ch === "\\" && i + 1 < raw.length && quote !== "'") {
32376
+ current += raw[++i];
32377
+ hasToken = true;
32378
+ continue;
32379
+ }
32380
+ if (quote) {
32381
+ if (ch === quote) {
32382
+ quote = null;
32383
+ } else {
32384
+ current += ch;
32385
+ }
32386
+ continue;
32387
+ }
32388
+ if (ch === '"' || ch === "'") {
32389
+ quote = ch;
32390
+ hasToken = true;
32391
+ continue;
32392
+ }
32393
+ if (/\s/.test(ch)) {
32394
+ if (hasToken) {
32395
+ tokens.push(current);
32396
+ current = "";
32397
+ hasToken = false;
32398
+ }
32399
+ continue;
32400
+ }
32401
+ current += ch;
32402
+ hasToken = true;
32403
+ }
32404
+ if (hasToken) tokens.push(current);
32405
+ return tokens;
32406
+ }
31871
32407
  async function browseStep(args2, opts, driver) {
31872
32408
  if (opts.stepFile) {
31873
32409
  try {
@@ -31888,9 +32424,17 @@ async function browseStep(args2, opts, driver) {
31888
32424
  if (Array.isArray(data.cmd)) {
31889
32425
  args2 = data.cmd.map(String);
31890
32426
  } else if (typeof data.cmd === "string") {
31891
- args2 = data.cmd.trim().split(/\s+/);
32427
+ args2 = parseActionCommand(data.cmd.trim());
31892
32428
  }
31893
32429
  }
32430
+ if (Array.isArray(data.then)) {
32431
+ const extra = data.then.map(
32432
+ (item) => Array.isArray(item) ? item.map(
32433
+ (tok) => /[\s"']/.test(String(tok)) ? JSON.stringify(String(tok)) : String(tok)
32434
+ ).join(" ") : String(item)
32435
+ );
32436
+ opts.then = [...opts.then ?? [], ...extra];
32437
+ }
31894
32438
  } catch (err) {
31895
32439
  return fail(
31896
32440
  `Failed to read step payload from ${opts.stepFile}: ${err.message}`,
@@ -31926,6 +32470,29 @@ async function browseStep(args2, opts, driver) {
31926
32470
  opts
31927
32471
  );
31928
32472
  }
32473
+ const chainedActions = [];
32474
+ if (opts.then && opts.then.length > 0) {
32475
+ for (const rawCmd of opts.then) {
32476
+ const tokens = parseActionCommand(rawCmd.trim());
32477
+ if (tokens.length === 0) continue;
32478
+ const subName = tokens[0];
32479
+ const subArgs = tokens.slice(1);
32480
+ const subAction = findPageAction(subName);
32481
+ if (!subAction) {
32482
+ return fail(
32483
+ `Unknown chained browse action "${subName}" in --then. Supported: ${Object.keys(PAGE_ACTIONS).join(", ")}.`,
32484
+ opts
32485
+ );
32486
+ }
32487
+ if (subArgs.length < subAction.minArgs) {
32488
+ return fail(
32489
+ `Usage for chained --then action: ${subAction.usage}`,
32490
+ opts
32491
+ );
32492
+ }
32493
+ chainedActions.push({ action: subAction, args: subArgs });
32494
+ }
32495
+ }
31929
32496
  const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
31930
32497
  if (!runDirPath) return;
31931
32498
  const maxSteps = parseMaxSteps(opts.maxSteps);
@@ -31937,12 +32504,25 @@ async function browseStep(args2, opts, driver) {
31937
32504
  "screenshots",
31938
32505
  `.pending-${randomUUID11()}.png`
31939
32506
  );
32507
+ let stepSnapshot;
31940
32508
  try {
31941
32509
  const page = await withPage(
31942
32510
  { ...opts, shot: pendingShot },
31943
32511
  driver,
31944
- (p) => action.run(p, actionArgs, opts),
31945
- action.target?.(actionArgs)
32512
+ async (p) => {
32513
+ await action.run(p, actionArgs, opts);
32514
+ for (const sub of chainedActions) {
32515
+ await sub.action.run(p, sub.args, opts);
32516
+ }
32517
+ },
32518
+ action.target?.(actionArgs),
32519
+ opts.snapshot !== false ? async (p) => {
32520
+ if (typeof p.locator === "function") {
32521
+ stepSnapshot = await captureSnapshotWithFrames(p, true).catch(
32522
+ () => void 0
32523
+ );
32524
+ }
32525
+ } : void 0
31946
32526
  );
31947
32527
  if (!page) return;
31948
32528
  let stagnationWarning;
@@ -31977,6 +32557,7 @@ async function browseStep(args2, opts, driver) {
31977
32557
  action: opts.action,
31978
32558
  note: effectiveNote,
31979
32559
  shot: page.screenshot,
32560
+ moveShot: true,
31980
32561
  url: page.url,
31981
32562
  surface: opts.surface ?? page.title ?? null,
31982
32563
  maxSteps
@@ -31986,7 +32567,8 @@ async function browseStep(args2, opts, driver) {
31986
32567
  record,
31987
32568
  ctx.projectRoot,
31988
32569
  stagnationWarning,
31989
- challengeWarning
32570
+ challengeWarning,
32571
+ stepSnapshot
31990
32572
  );
31991
32573
  if (record.cancelled) {
31992
32574
  if (!opts.json) {
@@ -32791,6 +33373,14 @@ import { spawnSync } from "child_process";
32791
33373
  import { existsSync as existsSync18, promises as fs61 } from "fs";
32792
33374
  import path58 from "path";
32793
33375
  import chalk32 from "chalk";
33376
+ function detectPackageManager(targetDir) {
33377
+ if (existsSync18(path58.join(targetDir, "pnpm-lock.yaml"))) return "pnpm";
33378
+ if (existsSync18(path58.join(targetDir, "yarn.lock"))) return "yarn";
33379
+ if (existsSync18(path58.join(targetDir, "bun.lockb")) || existsSync18(path58.join(targetDir, "bun.lock"))) {
33380
+ return "bun";
33381
+ }
33382
+ return "npm";
33383
+ }
32794
33384
  async function detectPreflightCommand(targetDir) {
32795
33385
  const pkgPath = path58.join(targetDir, "package.json");
32796
33386
  if (existsSync18(pkgPath)) {
@@ -32799,10 +33389,8 @@ async function detectPreflightCommand(targetDir) {
32799
33389
  const pkg = JSON.parse(raw);
32800
33390
  const scripts = pkg.scripts || {};
32801
33391
  if (scripts.typecheck) {
32802
- return { command: "npm", args: ["run", "typecheck"] };
32803
- }
32804
- if (scripts.check) {
32805
- return { command: "npm", args: ["run", "check"] };
33392
+ const pm = detectPackageManager(targetDir);
33393
+ return { command: pm, args: ["run", "typecheck"] };
32806
33394
  }
32807
33395
  } catch {
32808
33396
  }
@@ -32811,11 +33399,6 @@ async function detectPreflightCommand(targetDir) {
32811
33399
  if (existsSync18(tsconfigPath)) {
32812
33400
  return { command: "npx", args: ["--no-install", "tsc", "--noEmit"] };
32813
33401
  }
32814
- const biomePath = path58.join(targetDir, "biome.json");
32815
- const biomecPath = path58.join(targetDir, "biome.jsonc");
32816
- if (existsSync18(biomePath) || existsSync18(biomecPath)) {
32817
- return { command: "npx", args: ["--no-install", "biome", "check"] };
32818
- }
32819
33402
  return null;
32820
33403
  }
32821
33404
  async function runPreflightCheck(targetDir) {
@@ -32824,7 +33407,7 @@ async function runPreflightCheck(targetDir) {
32824
33407
  return {
32825
33408
  ok: true,
32826
33409
  skipped: true,
32827
- reason: "No typecheck or lint scripts detected in project"
33410
+ reason: "No typecheck script or tsconfig.json detected in project"
32828
33411
  };
32829
33412
  }
32830
33413
  const cmdString = `${detected.command} ${detected.args.join(" ")}`;
@@ -32832,20 +33415,22 @@ async function runPreflightCheck(targetDir) {
32832
33415
  cwd: targetDir,
32833
33416
  encoding: "utf8",
32834
33417
  timeout: 3e4,
32835
- stdio: ["ignore", "pipe", "pipe"]
33418
+ stdio: ["ignore", "pipe", "pipe"],
33419
+ shell: process.platform === "win32"
32836
33420
  });
32837
- const output = (proc.stdout || "") + (proc.stderr || "");
32838
- if (proc.status === 0) {
33421
+ const combined = ((proc.stdout || "") + (proc.stderr || "")).trim();
33422
+ const output = combined || (proc.error ? proc.error.message : "") || `Process exited with code ${proc.status ?? 1}`;
33423
+ if (proc.status === 0 && !proc.error) {
32839
33424
  return {
32840
33425
  ok: true,
32841
33426
  command: cmdString,
32842
- output: output.trim()
33427
+ output: combined
32843
33428
  };
32844
33429
  }
32845
33430
  return {
32846
33431
  ok: false,
32847
33432
  command: cmdString,
32848
- output: output.trim() || `Process exited with code ${proc.status ?? 1}`
33433
+ output
32849
33434
  };
32850
33435
  }
32851
33436
  async function preflight(opts = {}) {
@@ -32861,7 +33446,7 @@ async function preflight(opts = {}) {
32861
33446
  skipped: !!result.skipped,
32862
33447
  command: result.command ?? null,
32863
33448
  reason: result.reason ?? null,
32864
- message: result.skipped ? "No typecheck or lint script detected; preflight skipped" : "Preflight check passed with 0 errors"
33449
+ message: result.skipped ? "No typecheck script detected; preflight skipped" : "Preflight check passed with 0 errors"
32865
33450
  },
32866
33451
  null,
32867
33452
  2
@@ -32871,7 +33456,7 @@ async function preflight(opts = {}) {
32871
33456
  }
32872
33457
  if (result.skipped) {
32873
33458
  console.log(
32874
- chalk32.dim("\u25C7 Preflight skipped: no typecheck or lint scripts detected")
33459
+ chalk32.dim("\u25C7 Preflight skipped: no typecheck script detected")
32875
33460
  );
32876
33461
  } else {
32877
33462
  console.log(
@@ -32897,9 +33482,7 @@ ${result.output}`,
32897
33482
  console.error(chalk32.dim(result.output));
32898
33483
  }
32899
33484
  console.error(
32900
- chalk32.yellow(
32901
- "\nFix compilation/lint errors before starting a walkthrough."
32902
- )
33485
+ chalk32.yellow("\nFix compilation errors before starting a walkthrough.")
32903
33486
  );
32904
33487
  process.exit(1);
32905
33488
  }
@@ -32911,7 +33494,7 @@ var init_preflight = __esm({
32911
33494
  });
32912
33495
 
32913
33496
  // src/worktree-state.ts
32914
- import { execFileSync } from "child_process";
33497
+ import { execFileSync as execFileSync2 } from "child_process";
32915
33498
  import { createHash as createHash6 } from "crypto";
32916
33499
  import {
32917
33500
  closeSync as closeSync3,
@@ -32923,7 +33506,7 @@ import {
32923
33506
  } from "fs";
32924
33507
  import path59 from "path";
32925
33508
  function git(args2, cwd) {
32926
- return execFileSync("git", args2, {
33509
+ return execFileSync2("git", args2, {
32927
33510
  cwd,
32928
33511
  stdio: ["ignore", "pipe", "ignore"],
32929
33512
  timeout: GIT_TIMEOUT_MS2,
@@ -33056,7 +33639,7 @@ var init_worktree_state = __esm({
33056
33639
  });
33057
33640
 
33058
33641
  // src/build-info.ts
33059
- import { execFileSync as execFileSync2 } from "child_process";
33642
+ import { execFileSync as execFileSync3 } from "child_process";
33060
33643
  import { readFileSync as readFileSync8 } from "fs";
33061
33644
  function nonEmpty(value) {
33062
33645
  const v = value?.trim();
@@ -33155,7 +33738,7 @@ var init_build_info = __esm({
33155
33738
  SHA_RE = /^[0-9a-f]{7,64}$/i;
33156
33739
  runGit = (args2, cwd) => {
33157
33740
  try {
33158
- const out2 = execFileSync2("git", args2, {
33741
+ const out2 = execFileSync3("git", args2, {
33159
33742
  cwd,
33160
33743
  encoding: "utf8",
33161
33744
  stdio: ["ignore", "pipe", "ignore"],
@@ -33458,6 +34041,13 @@ async function listSingleType(projectRoot, type2, capabilityFilter, diffFilter,
33458
34041
  {
33459
34042
  header: "outline",
33460
34043
  value: (e) => e.frontmatter.outline ?? ""
34044
+ },
34045
+ {
34046
+ header: "entry",
34047
+ value: (e) => {
34048
+ const sfm = e.frontmatter;
34049
+ return chalk33.dim(sfm.entry_path ?? "/");
34050
+ }
33461
34051
  }
33462
34052
  ]);
33463
34053
  break;
@@ -34431,6 +35021,9 @@ function applyChanges(type2, fm, opts) {
34431
35021
  opts.untestableBoundaries ? splitList(opts.untestableBoundaries) : null
34432
35022
  );
34433
35023
  }
35024
+ if ((opts.target || opts.authStrategy || opts.access) && fm.status !== "verified") {
35025
+ fm.status = "verified";
35026
+ }
34434
35027
  } else if (type2 === "context") {
34435
35028
  if (opts.domain !== void 0) put("domain", opts.domain || null);
34436
35029
  if (opts.industry !== void 0) put("industry", opts.industry || null);
@@ -34946,21 +35539,39 @@ async function editEntity(type2, slug, opts = {}) {
34946
35539
  const ctx = await resolveProject(opts);
34947
35540
  if (!ctx) return;
34948
35541
  const { projectRoot } = ctx;
34949
- if (!ENTITY_TYPES.includes(type2)) {
35542
+ const isSpecial = type2 === "testability" || type2 === "context";
35543
+ if (!isSpecial && !ENTITY_TYPES.includes(type2)) {
34950
35544
  commandError(
34951
- `Unknown entity type "${type2}". Supported: ${ENTITY_TYPES.join(", ")}.`,
35545
+ `Unknown entity type "${type2}". Supported: ${EDITABLE_TYPES.join(", ")}.`,
34952
35546
  { ...opts, code: "unknown_entity_type", fix: "iamu list" }
34953
35547
  );
34954
35548
  return;
34955
35549
  }
34956
- const dir = ENTITY_DIRS[type2];
34957
- const filePath = path62.join(projectRoot, "iamu", dir, `${slug}.md`);
35550
+ if (!isSpecial && !slug) {
35551
+ commandError(`A slug is required when editing a ${type2}.`, {
35552
+ ...opts,
35553
+ code: "missing_slug",
35554
+ fix: `iamu edit ${type2} <slug>`
35555
+ });
35556
+ return;
35557
+ }
35558
+ const effectiveSlug = isSpecial ? type2 : slug;
35559
+ const filePath = type2 === "testability" ? testabilityPath(projectRoot) : type2 === "context" ? contextPath(projectRoot) : path62.join(
35560
+ projectRoot,
35561
+ "iamu",
35562
+ ENTITY_DIRS[type2],
35563
+ `${effectiveSlug}.md`
35564
+ );
34958
35565
  try {
34959
35566
  await fs64.access(filePath);
34960
35567
  } catch {
34961
35568
  commandError(
34962
- `${type2} "${slug}" not found at ${path62.relative(projectRoot, filePath)}.`,
34963
- { ...opts, code: "entity_not_found", fix: `iamu list ${type2}s` }
35569
+ `${type2}${isSpecial ? "" : ` "${effectiveSlug}"`} not found at ${path62.relative(projectRoot, filePath)}.`,
35570
+ {
35571
+ ...opts,
35572
+ code: "entity_not_found",
35573
+ fix: isSpecial ? "iamu init" : `iamu list ${type2}s`
35574
+ }
34964
35575
  );
34965
35576
  return;
34966
35577
  }
@@ -34970,7 +35581,7 @@ async function editEntity(type2, slug, opts = {}) {
34970
35581
  {
34971
35582
  ...opts,
34972
35583
  code: "non_interactive_environment",
34973
- fix: `iamu set ${type2} ${slug}`
35584
+ fix: isSpecial ? `iamu set ${type2}` : `iamu set ${type2} ${effectiveSlug}`
34974
35585
  }
34975
35586
  );
34976
35587
  return;
@@ -34983,7 +35594,8 @@ async function editEntity(type2, slug, opts = {}) {
34983
35594
  const editorArgs = parts.slice(1).map((a) => a.replace(/^["']|["']$/g, ""));
34984
35595
  await new Promise((resolve3) => {
34985
35596
  const child = spawn2(editorCmd, [...editorArgs, filePath], {
34986
- stdio: "inherit"
35597
+ stdio: "inherit",
35598
+ shell: process.platform === "win32"
34987
35599
  });
34988
35600
  child.on("exit", (code) => {
34989
35601
  if (code !== 0) {
@@ -35013,7 +35625,7 @@ async function editEntity(type2, slug, opts = {}) {
35013
35625
  try {
35014
35626
  const content = await fs64.readFile(filePath, "utf8");
35015
35627
  const { frontmatter } = parseFrontmatter(content);
35016
- const schema2 = schemaByType[type2];
35628
+ const schema2 = type2 === "testability" ? testabilityFrontmatterSchema : type2 === "context" ? contextFrontmatterSchema : schemaByType[type2];
35017
35629
  const parsed = schema2.safeParse(frontmatter);
35018
35630
  if (!parsed.success) {
35019
35631
  if (opts.json) {
@@ -35021,7 +35633,7 @@ async function editEntity(type2, slug, opts = {}) {
35021
35633
  JSON.stringify({
35022
35634
  valid: false,
35023
35635
  type: type2,
35024
- slug,
35636
+ slug: effectiveSlug,
35025
35637
  path: filePath,
35026
35638
  errors: parsed.error.issues.map((i) => ({
35027
35639
  field: i.path.join(".") || "root",
@@ -35032,7 +35644,7 @@ async function editEntity(type2, slug, opts = {}) {
35032
35644
  } else {
35033
35645
  console.log();
35034
35646
  console.log(
35035
- ` ${symbols.warning} ${chalk38.yellow(`Validation failed for ${type2} "${slug}":`)}`
35647
+ ` ${symbols.warning} ${chalk38.yellow(`Validation failed for ${type2}${isSpecial ? "" : ` "${effectiveSlug}"`}:`)}`
35036
35648
  );
35037
35649
  for (const issue of parsed.error.issues) {
35038
35650
  const field2 = issue.path.join(".") || "root";
@@ -35050,15 +35662,24 @@ async function editEntity(type2, slug, opts = {}) {
35050
35662
  return;
35051
35663
  }
35052
35664
  if (opts.json) {
35053
- console.log(JSON.stringify({ valid: true, type: type2, slug, path: filePath }));
35665
+ console.log(
35666
+ JSON.stringify({
35667
+ valid: true,
35668
+ type: type2,
35669
+ slug: effectiveSlug,
35670
+ path: filePath
35671
+ })
35672
+ );
35054
35673
  return;
35055
35674
  }
35056
35675
  console.log();
35057
- console.log(` ${symbols.success} Validated ${type2} ${chalk38.cyan(slug)}`);
35676
+ console.log(
35677
+ ` ${symbols.success} Validated ${type2}${isSpecial ? "" : ` ${chalk38.cyan(effectiveSlug)}`}`
35678
+ );
35058
35679
  console.log();
35059
35680
  } catch (err) {
35060
35681
  commandError(
35061
- `Failed to validate ${type2} "${slug}" after edit: ${err instanceof Error ? err.message : String(err)}`,
35682
+ `Failed to validate ${type2}${isSpecial ? "" : ` "${effectiveSlug}"`} after edit: ${err instanceof Error ? err.message : String(err)}`,
35062
35683
  {
35063
35684
  ...opts,
35064
35685
  code: "validation_error",
@@ -35067,6 +35688,7 @@ async function editEntity(type2, slug, opts = {}) {
35067
35688
  );
35068
35689
  }
35069
35690
  }
35691
+ var EDITABLE_TYPES;
35070
35692
  var init_edit_entity = __esm({
35071
35693
  "src/commands/edit-entity.ts"() {
35072
35694
  "use strict";
@@ -35075,6 +35697,7 @@ var init_edit_entity = __esm({
35075
35697
  init_symbols();
35076
35698
  init_context4();
35077
35699
  init_validate();
35700
+ EDITABLE_TYPES = [...ENTITY_TYPES, "testability", "context"];
35078
35701
  }
35079
35702
  });
35080
35703
 
@@ -35125,7 +35748,7 @@ async function settle(page, settleMs) {
35125
35748
  });
35126
35749
  await page.waitForLoadState("networkidle", { timeout: settleMs }).catch(() => {
35127
35750
  });
35128
- const spaSettle = Math.min(Math.max(settleMs, 50), 200);
35751
+ const spaSettle = Math.min(Math.max(settleMs, 25), 60);
35129
35752
  await page.waitForTimeout(spaSettle).catch(() => {
35130
35753
  });
35131
35754
  }
@@ -35193,7 +35816,7 @@ async function crawl(page, opts) {
35193
35816
  }
35194
35817
  async function crawlFrom(page, opts, origin, takeBlocked) {
35195
35818
  const deny = opts.deny ?? [];
35196
- const settleMs = opts.settleMs ?? 1500;
35819
+ const settleMs = opts.settleMs ?? 400;
35197
35820
  const { budget } = opts;
35198
35821
  const stats = {
35199
35822
  surfaces: 0,
@@ -36354,12 +36977,13 @@ async function startRun(scenarioSlugArg, opts) {
36354
36977
  return;
36355
36978
  }
36356
36979
  const fallbackTarget = await testabilityTarget2(projectRoot);
36980
+ const targetSource = opts.target ? "--target" : fm.target_url ? `scenario (${scenarioSlug})` : fallbackTarget ? "iamu/testability.md" : null;
36357
36981
  const start2 = resolveStartUrl(opts.target, fm, fallbackTarget);
36358
36982
  if ("error" in start2) {
36359
36983
  commandError(start2.error, {
36360
36984
  ...opts,
36361
36985
  code: "invalid_target_url",
36362
- fix: `iamu set scenario ${scenarioSlug} --target <url>`
36986
+ fix: "iamu set testability --target <url>"
36363
36987
  });
36364
36988
  return;
36365
36989
  }
@@ -36370,12 +36994,13 @@ async function startRun(scenarioSlugArg, opts) {
36370
36994
  const probe = await checkTargetReachable(start2.url);
36371
36995
  targetReachable = probe.reachable;
36372
36996
  if (!probe.reachable) {
36373
- targetWarning = `Target URL "${start2.url}" is unreachable (${probe.error ?? "connection refused"}). Ensure your product/dev server is running before recording steps.`;
36997
+ const fixCmd = fm.target_url ? `iamu set testability --target <url> && iamu set scenario ${scenarioSlug} --no-target` : "iamu set testability --target <url>";
36998
+ targetWarning = `Target URL "${start2.url}" is unreachable (${probe.error ?? "connection refused"}). Ensure your dev server is running, or update the URL with: ${fixCmd}`;
36374
36999
  if (opts.check === true) {
36375
37000
  commandError(targetWarning, {
36376
37001
  ...opts,
36377
37002
  code: "target_unreachable",
36378
- fix: `curl -I ${start2.url}`
37003
+ fix: fixCmd
36379
37004
  });
36380
37005
  return;
36381
37006
  }
@@ -36465,6 +37090,7 @@ async function startRun(scenarioSlugArg, opts) {
36465
37090
  path: path64.relative(projectRoot, run.absPath),
36466
37091
  scenario_slug: scenarioSlug,
36467
37092
  target_url: start2.url,
37093
+ target_source: targetSource,
36468
37094
  target_reachable: targetReachable,
36469
37095
  ...targetWarning ? { target_warning: targetWarning } : {},
36470
37096
  ...liveRes?.viewerUrl ? { live_viewer_url: liveRes.viewerUrl } : {},
@@ -36493,7 +37119,9 @@ async function startRun(scenarioSlugArg, opts) {
36493
37119
  console.log(` ${chalk40.dim("scenario:")} ${scenarioSlug}`);
36494
37120
  console.log(` ${chalk40.dim("persona:")} ${fm.persona}`);
36495
37121
  console.log(` ${chalk40.dim("outline:")} ${fm.outline}`);
36496
- console.log(` ${chalk40.dim("target:")} ${start2.url}`);
37122
+ console.log(
37123
+ ` ${chalk40.dim("target:")} ${start2.url}${targetSource ? chalk40.dim(` (from ${targetSource})`) : ""}`
37124
+ );
36497
37125
  console.log(
36498
37126
  ` ${chalk40.dim("models:")} walker: ${chalk40.cyan(walker)} \xB7 judge: ${chalk40.cyan(judge)} \xB7 matcher: ${chalk40.cyan(matcher)}`
36499
37127
  );
@@ -36569,14 +37197,18 @@ __export(instructions_walk_exports, {
36569
37197
  extractUntestableBoundaries: () => extractUntestableBoundaries,
36570
37198
  instructionsWalk: () => instructionsWalk,
36571
37199
  revisitSection: () => revisitSection,
37200
+ selfCommand: () => selfCommand,
36572
37201
  viewportLine: () => viewportLine
36573
37202
  });
36574
37203
  import { existsSync as existsSync20, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
36575
37204
  import { join as join10 } from "path";
36576
37205
  import chalk41 from "chalk";
36577
- function selfCommand() {
36578
- const q = (p) => /[\s"'$]/.test(p) ? JSON.stringify(p) : p;
36579
- return `${q(process.execPath)} ${q(process.argv[1] ?? "iamu")}`;
37206
+ function selfCommand(execPath = process.execPath, scriptPath = process.argv[1] ?? "iamu") {
37207
+ const q = (p) => {
37208
+ const norm = p.replace(/\\/g, "/");
37209
+ return /[\s"'$]/.test(norm) ? JSON.stringify(norm) : norm;
37210
+ };
37211
+ return `${q(execPath)} ${q(scriptPath)}`;
36580
37212
  }
36581
37213
  function viewportLine(v) {
36582
37214
  return `The browser is sized as ${SCREEN[v.device]}, ${v.width}\xD7${v.height}.`;
@@ -36585,18 +37217,20 @@ function browseSurface(iamu, viewport) {
36585
37217
  return [
36586
37218
  "`iamu browse` \u2014 a visible Chrome the orchestrator has already started.",
36587
37219
  "",
36588
- "Drive and record each step atomically in one command:",
37220
+ "Drive and record each step atomically in one command (automatically saves the screenshot AND prints the updated `[#N]` interactive elements + accessibility snapshot):",
36589
37221
  ` ${iamu} browse step --run <run-id> --action "<action>" --note "<monologue>" <command> [args...]`,
36590
37222
  "",
37223
+ "Chain consecutive form fills or actions on the same screen into a single step with `--then` (repeatable):",
37224
+ ` ${iamu} browse step --run <run-id> --action "<action>" --note "<monologue>" fill-index 21 "2026-11-09" --then 'fill-index 22 "2026-11-20"'`,
37225
+ "",
36591
37226
  "Supported commands (run with step or standalone):",
36592
- ` ${iamu} browse goto <url>`,
36593
- ` ${iamu} browse snapshot # ARIA roles + accessible names = what you see`,
36594
- ` ${iamu} browse snapshot --indexed # Numbered interactive elements [#1], [#2]`,
37227
+ ` ${iamu} browse goto <url> # Opens URL and prints [#N] elements + snapshot`,
37228
+ ` ${iamu} browse snapshot --indexed # Re-inspect current page (not needed after goto or step)`,
37229
+ ` ${iamu} browse click-index <number> # Click element by [#N] index (preferred, fast)`,
37230
+ ` ${iamu} browse fill-index <number> "<text>" [--submit]`,
36595
37231
  ` ${iamu} browse click <role> "<name>" [--nth N]`,
36596
37232
  ` ${iamu} browse click-text "<visible text>"`,
36597
- ` ${iamu} browse click-index <number> # Click element by [#N] index`,
36598
37233
  ` ${iamu} browse fill <role> "<name>" "<text>" [--submit]`,
36599
- ` ${iamu} browse fill-index <number> "<text>" [--submit]`,
36600
37234
  ` ${iamu} browse select <role> "<name>" "<option>"`,
36601
37235
  ` ${iamu} browse upload <role> "<name>" "<file-path>"`,
36602
37236
  ` ${iamu} browse upload-index <number> "<file-path>"`,
@@ -36609,7 +37243,11 @@ function browseSurface(iamu, viewport) {
36609
37243
  ` ${iamu} browse shot <png> [--full]`,
36610
37244
  ` ${iamu} browse wait [ms] # Wait for page/animations to settle`,
36611
37245
  "",
36612
- "Look at screenshots with your file-reading tool. Do not run `browse start` or `browse stop`, and do not drive the product any other way. The browser gives you no sound: you may report that an audio control gave no visible confirmation, never what the audio said.",
37246
+ "Fast-driving rules:",
37247
+ "\u2022 `browse goto` and `browse step` automatically print the fresh `[#N]` interactive elements and accessibility snapshot. Do NOT run `browse snapshot` after `browse goto` or `browse step`.",
37248
+ "\u2022 Prefer `click-index <N>` and `fill-index <N>` using the `[#N]` indices from the previous step's output.",
37249
+ "\u2022 Do NOT read step screenshot PNG files with your file-reading tool after routine steps \u2014 the snapshot output already shows all text, roles, and states. Only read a screenshot file if you suspect a visual rendering issue that contradicts the snapshot.",
37250
+ "\u2022 Do not run `browse start` or `browse stop`, and do not drive the product any other way. The browser gives you no sound: you may report that an audio control gave no visible confirmation, never what the audio said.",
36613
37251
  ...viewport ? [viewportLine(viewport)] : []
36614
37252
  ].join("\n");
36615
37253
  }
@@ -36746,14 +37384,14 @@ function composeWalkerPrompt(p) {
36746
37384
  "The CLI handles step numbering, timestamps, screenshot capture/linking, and telemetry automatically.",
36747
37385
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
36748
37386
  `Record findings with: ${iamu} new-finding --scenario ${p.scenarioSlug} --run ${p.runId} --step <N> --title "..." --type <helps|hinders|missing|irrelevant|incorrect> --severity <critical|major|minor|nit> --ui-fact "..." --consequence "..." --task-impact <blocks|impairs|slows|neutral|accelerates|enables> --confidence <high|medium|low> --need "<what you needed, no solution>" --description "..." --json`,
36749
- "`--step <N>` is the step whose screenshot shows the finding (or strength): it is the evidence people see next to the finding, so record that step and its screenshot first, and pick the screenshot where the thing is actually visible.",
37387
+ "`--step <N>` is the step whose screenshot shows the finding (or strength): it is the evidence people see next to the finding, so record that step and its screenshot first, and pick the screenshot where the thing is actually visible. When recording multiple findings at the end of a screen or walk, batch them in a single shell command chained with `&&` (and you may chain `iamu summary` after them) rather than running one finding per turn.",
36750
37388
  `Before you finish, record synthesis with: ${iamu} summary ${p.runId} --reached "..." | --stopped "..." --worked "..." --worked "..." --question "..." [--setup-note "..."]. --reached: you got to what you came to do (the last thing you did and what the product showed); --stopped: you ended before it, and why. One of the two, always. --worked: what worked for this persona, one point each. --question: decisions for the product team the walk raised. --setup-note: doubts about the test setup (missing data, accounts, dates), never the product.`,
36751
37389
  `If the walk cannot start as it was set up \u2014 you cannot become this persona (the switch or login fails, or you are someone else), the world the situation needs is not there and you cannot create it, or the starting page does not load \u2014 stop immediately and end the walk with: ${iamu} summary ${p.runId} --blocked "<what was missing, as you saw it>". Do NOT record preliminary findings if preconditions are not met. That is about the setup, not the product: when the product has no way to do what you need, that is a finding (--type missing), not a block. Do not record findings about the missing setup itself.`,
36752
37390
  "Execute everything via CLI commands in the shell. Never invoke host Edit or Write tools on files in the workspace.",
36753
37391
  "",
36754
37392
  "Do not read the product's source code, other iamu/ entity files, or earlier runs: you know only what the persona knows.",
36755
37393
  "",
36756
- "When done, return a summary of steps taken and findings recorded."
37394
+ "When done, reply with a concise one-line status (`Walk complete: <N> steps, <M> findings`) \u2014 do not re-type the full findings or synthesis into chat, as the orchestrator reads them directly from disk."
36757
37395
  ].join("\n");
36758
37396
  }
36759
37397
  async function instructionsWalk(opts) {
@@ -39726,13 +40364,19 @@ function registerFindingsCommands(program) {
39726
40364
 
39727
40365
  // src/commands/register-project.ts
39728
40366
  function registerProjectCommands(program) {
39729
- program.command("init").helpGroup("Project:").description("Scaffold iamu/ + install skills").argument("[name]", "Project name (defaults to directory name)").option("-y, --yes", "Skip prompts and accept all detected defaults").option("--cloud", "Connect to Iamu Cloud directly during init").option("--no-cloud", "Skip Iamu Cloud connection (local-only)").option("--json", "Output initialization metadata as JSON").addHelpText(
40367
+ program.command("init").helpGroup("Project:").description("Scaffold iamu/ + install skills").argument("[name]", "Project name (defaults to directory name)").option("-y, --yes", "Skip prompts and accept all detected defaults").option("--target <url>", "Target URL for local or staging walkthroughs").option("--cloud", "Connect to Iamu Cloud directly during init").option("--no-cloud", "Skip Iamu Cloud connection (local-only)").option("--json", "Output initialization metadata as JSON").addHelpText(
39730
40368
  "after",
39731
- "\nExamples:\n $ iamu init\n $ iamu init my-project -y\n $ iamu init --cloud\n $ iamu init --json"
40369
+ "\nExamples:\n $ iamu init\n $ iamu init my-project -y\n $ iamu init --target http://localhost:5173\n $ iamu init --cloud\n $ iamu init --json"
39732
40370
  ).action(
39733
40371
  async (name, opts) => {
39734
40372
  const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
39735
- await init2({ name, yes: opts.yes, json: opts.json, cloud: opts.cloud });
40373
+ await init2({
40374
+ name,
40375
+ yes: opts.yes,
40376
+ json: opts.json,
40377
+ cloud: opts.cloud,
40378
+ target: opts.target
40379
+ });
39736
40380
  }
39737
40381
  );
39738
40382
  program.command("status").helpGroup("Project:").description("Show project state: entities, runs, context, cloud link").option("--json", "Output as JSON").addHelpText(
@@ -39754,7 +40398,7 @@ function registerProjectCommands(program) {
39754
40398
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
39755
40399
  await doctor2(opts);
39756
40400
  });
39757
- program.command("preflight").helpGroup("Project:").description("Run fast typecheck/lint preflight gate before walkthroughs").option("--cwd <path>", "Working directory of target project").option("--json", "Output diagnostics as JSON").addHelpText(
40401
+ program.command("preflight").helpGroup("Project:").description("Run fast typecheck check before walkthroughs").option("--cwd <path>", "Working directory of target project").option("--json", "Output diagnostics as JSON").addHelpText(
39758
40402
  "after",
39759
40403
  "\nExamples:\n $ iamu preflight\n $ iamu preflight --json"
39760
40404
  ).action(async (opts) => {
@@ -39894,8 +40538,8 @@ function registerProjectCommands(program) {
39894
40538
  await setEntity2(type2, slug, opts);
39895
40539
  }
39896
40540
  );
39897
- program.command("edit <type> <slug>").helpGroup("Project:").description(
39898
- "Open an entity file in your editor ($VISUAL or $EDITOR) and validate on exit"
40541
+ program.command("edit <type> [slug]").helpGroup("Project:").description(
40542
+ "Open an entity or project file (testability, context) in your editor ($VISUAL or $EDITOR) and validate on exit"
39899
40543
  ).option("--json", "Output as JSON").action(
39900
40544
  async (type2, slug, opts = {}) => {
39901
40545
  const { editEntity: editEntity2 } = await Promise.resolve().then(() => (init_edit_entity(), edit_entity_exports));
@@ -40084,6 +40728,13 @@ function registerWalkthroughCommands(program) {
40084
40728
  ).option("--action <text>", "Action description (step only)").option("--note <text>", "Persona inner monologue / reasoning (step only)").option("--surface <name>", "Screen or surface name (step only)").option(
40085
40729
  "--step-file <path>",
40086
40730
  "Read step payload (action, note, cmd) from a JSON file or '-' for stdin (step only)"
40731
+ ).option(
40732
+ "--then <cmd>",
40733
+ "Chain an additional browser action in the same step before taking the screenshot (repeatable, step only)",
40734
+ (value, previous = []) => [...previous, value]
40735
+ ).option(
40736
+ "--no-snapshot",
40737
+ "Omit the post-action accessibility snapshot from browse step / goto output"
40087
40738
  ).option("--width <px>", "Window width (start)").option("--height <px>", "Window height (start)").option("--headless", "Run without a window (start)").option(
40088
40739
  "--port <port>",
40089
40740
  "CDP port (start), default 9333",