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