@iamu-dev/iamu 0.2.0-beta.5 → 0.2.0-beta.6

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
@@ -200,17 +200,24 @@ function defaultConfig(params) {
200
200
  cloud_api_url: null
201
201
  };
202
202
  }
203
- var baseProjectFields, localOnlyConfig, cloudLinkedConfig, projectConfigSchema;
203
+ var modelTierSchema, projectModelsConfigSchema, baseProjectFields, localOnlyConfig, cloudLinkedConfig, projectConfigSchema;
204
204
  var init_config = __esm({
205
205
  "../../packages/protocol/dist/schemas/frontmatter/config.js"() {
206
206
  "use strict";
207
207
  init_common();
208
208
  init_version();
209
+ modelTierSchema = z4.string().min(1).max(128).describe("Model role tier ('fast', 'thorough', 'inherit') or a specific model identifier.");
210
+ projectModelsConfigSchema = z4.object({
211
+ walker: modelTierSchema.optional(),
212
+ matcher: modelTierSchema.optional(),
213
+ judge: modelTierSchema.optional()
214
+ }).strict();
209
215
  baseProjectFields = {
210
216
  schema_version: z4.literal(SCHEMA_VERSION),
211
217
  id: uuidSchema,
212
218
  project_slug: slugSchema,
213
- project_name: z4.string().min(1)
219
+ project_name: z4.string().min(1),
220
+ models: projectModelsConfigSchema.optional()
214
221
  };
215
222
  localOnlyConfig = z4.object({
216
223
  ...baseProjectFields,
@@ -1154,7 +1161,21 @@ var init_walkthrough_bundle = __esm({
1154
1161
  judge_prompt: z19.string().optional(),
1155
1162
  judge_model: z19.string().optional()
1156
1163
  }).optional()
1157
- }).optional()
1164
+ }).optional(),
1165
+ models: z19.object({
1166
+ walker: z19.string().optional(),
1167
+ matcher: z19.string().optional(),
1168
+ judge: z19.string().optional(),
1169
+ walker_reported: z19.string().optional(),
1170
+ matcher_reported: z19.string().optional(),
1171
+ judge_reported: z19.string().optional(),
1172
+ walker_tier: z19.string().optional(),
1173
+ matcher_tier: z19.string().optional(),
1174
+ judge_tier: z19.string().optional(),
1175
+ execution_mode: z19.enum(["multi_agent", "single_agent"]).optional(),
1176
+ velocity_sec_per_step: z19.number().optional(),
1177
+ total_duration_sec: z19.number().optional()
1178
+ }).optional().describe("AI model configuration and reported runtime provenance for this walkthrough.")
1158
1179
  });
1159
1180
  walkthroughBundleSchema = z19.object({
1160
1181
  manifest: bundleManifestSchema,
@@ -1377,6 +1398,7 @@ var init_walkthrough_canonical = __esm({
1377
1398
  build: buildMetadataSchema.optional(),
1378
1399
  viewport: runViewportSchema.optional(),
1379
1400
  environment: z20.string().optional(),
1401
+ models: z20.record(z20.unknown()).optional(),
1380
1402
  findings: z20.array(canonicalFindingSchema),
1381
1403
  steps: z20.array(canonicalStepSchema),
1382
1404
  human_tests: z20.array(canonicalHumanTestSchema),
@@ -7248,6 +7270,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7248
7270
  ...manifest.build ? { build: manifest.build } : {},
7249
7271
  ...manifest.viewport ? { viewport: manifest.viewport } : {},
7250
7272
  ...manifest.environment ? { environment: manifest.environment } : {},
7273
+ ...manifest.models ? { models: manifest.models } : {},
7251
7274
  findings: canonicalFindings,
7252
7275
  steps: canonicalSteps,
7253
7276
  human_tests: humanTests,
@@ -7408,6 +7431,20 @@ function renderCanonicalMarkdown(canonical) {
7408
7431
  lines.push(`- Viewport: ${canonical.viewport.device} (${canonical.viewport.width}\xD7${canonical.viewport.height})`);
7409
7432
  }
7410
7433
  lines.push(`- Started: ${canonical.provenance.started_at}`, `- Completed: ${canonical.provenance.completed_at}`, `- Walker prompt: ${canonical.provenance.walker_prompt.id}@${canonical.provenance.walker_prompt.version}`, `- Findings: ${canonical.findings.length}`, `- Steps: ${canonical.steps.length}`);
7434
+ const models = canonical.models;
7435
+ if (models) {
7436
+ const parts = [
7437
+ models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
7438
+ models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
7439
+ models.matcher ? `Matcher: ${models.matcher}` : ""
7440
+ ].filter(Boolean);
7441
+ if (parts.length > 0) {
7442
+ lines.push(`- Models: ${parts.join(" \xB7 ")}`);
7443
+ }
7444
+ if (typeof models.velocity_sec_per_step === "number") {
7445
+ lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
7446
+ }
7447
+ }
7411
7448
  const what = canonical.what_worked;
7412
7449
  if (what && what.length > 0) {
7413
7450
  lines.push("", "## What worked");
@@ -7777,6 +7814,7 @@ async function assembleWalkthroughBundle(projectRoot, runId) {
7777
7814
  report_markdown: report3,
7778
7815
  verdict: verdictArray,
7779
7816
  judge_meta: judgeMeta,
7817
+ ...manifest.models ? { models: manifest.models } : {},
7780
7818
  ...buildFields(manifest),
7781
7819
  ...await readCanonicalExtras(projectRoot, runId)
7782
7820
  },
@@ -14434,6 +14472,18 @@ async function startRun(scenarioSlug, opts) {
14434
14472
  }
14435
14473
  const build = detectBuild({ cwd: projectRoot, label: opts.build });
14436
14474
  const environment = resolveEnvironment(opts.env, start2.url);
14475
+ const cfg = await readConfig(projectRoot).catch(() => null);
14476
+ const walker = opts.walker ?? process.env.IAMU_WALKER_MODEL ?? cfg?.models?.walker ?? "fast";
14477
+ const judge = opts.judge ?? process.env.IAMU_JUDGE_MODEL ?? cfg?.models?.judge ?? "thorough";
14478
+ const matcher = opts.matcher ?? process.env.IAMU_MATCHER_MODEL ?? cfg?.models?.matcher ?? "thorough";
14479
+ const models = {
14480
+ walker,
14481
+ judge,
14482
+ matcher,
14483
+ walker_tier: ["fast", "thorough", "inherit"].includes(walker) ? walker : "custom",
14484
+ judge_tier: ["fast", "thorough", "inherit"].includes(judge) ? judge : "custom",
14485
+ matcher_tier: ["fast", "thorough", "inherit"].includes(matcher) ? matcher : "custom"
14486
+ };
14437
14487
  let revisit = [];
14438
14488
  try {
14439
14489
  revisit = await computeRevisitPlaces(projectRoot, scenarioSlug, start2.url);
@@ -14451,6 +14501,7 @@ async function startRun(scenarioSlug, opts) {
14451
14501
  status: "running",
14452
14502
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
14453
14503
  completed_at: null,
14504
+ models,
14454
14505
  ...build ? { build } : {},
14455
14506
  ...environment ? { environment } : {},
14456
14507
  ...revisit.length > 0 ? { revisit } : {},
@@ -14472,6 +14523,7 @@ async function startRun(scenarioSlug, opts) {
14472
14523
  ...targetWarning ? { target_warning: targetWarning } : {},
14473
14524
  persona_slug: fm.persona,
14474
14525
  outline_slug: fm.outline,
14526
+ models,
14475
14527
  build: build ?? null,
14476
14528
  environment: environment ?? null,
14477
14529
  revisit,
@@ -14490,6 +14542,9 @@ async function startRun(scenarioSlug, opts) {
14490
14542
  console.log(` ${chalk11.dim("persona:")} ${fm.persona}`);
14491
14543
  console.log(` ${chalk11.dim("outline:")} ${fm.outline}`);
14492
14544
  console.log(` ${chalk11.dim("target:")} ${start2.url}`);
14545
+ console.log(
14546
+ ` ${chalk11.dim("models:")} walker: ${chalk11.cyan(walker)} \xB7 judge: ${chalk11.cyan(judge)} \xB7 matcher: ${chalk11.cyan(matcher)}`
14547
+ );
14493
14548
  if (targetWarning) {
14494
14549
  console.log(` ${symbols.warning} ${chalk11.yellow(targetWarning)}`);
14495
14550
  }
@@ -16673,6 +16728,7 @@ __export(browse_exports, {
16673
16728
  readBrowseState: () => readBrowseState,
16674
16729
  shouldDisableSandbox: () => shouldDisableSandbox,
16675
16730
  start: () => start,
16731
+ waitForVisualStability: () => waitForVisualStability,
16676
16732
  withPage: () => withPage
16677
16733
  });
16678
16734
  import { randomUUID as randomUUID7 } from "crypto";
@@ -16879,6 +16935,45 @@ async function stop(opts, driver) {
16879
16935
  }
16880
16936
  report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
16881
16937
  }
16938
+ async function waitForVisualStability(page, maxWaitMs = 300) {
16939
+ let timer;
16940
+ try {
16941
+ const nodeTimeout = new Promise((resolve4) => {
16942
+ timer = setTimeout(resolve4, maxWaitMs + 100);
16943
+ });
16944
+ const browserWait = page.evaluate((maxWait) => {
16945
+ return new Promise((resolve4) => {
16946
+ const timeout = setTimeout(resolve4, maxWait);
16947
+ const done = () => {
16948
+ clearTimeout(timeout);
16949
+ resolve4();
16950
+ };
16951
+ const g = globalThis;
16952
+ if (typeof g.requestAnimationFrame !== "function") {
16953
+ done();
16954
+ return;
16955
+ }
16956
+ g.requestAnimationFrame(() => {
16957
+ g.requestAnimationFrame?.(() => {
16958
+ const animations = g.document?.getAnimations ? g.document.getAnimations().filter((a) => a.playState === "running" && a.finished) : [];
16959
+ if (animations.length === 0) {
16960
+ done();
16961
+ return;
16962
+ }
16963
+ Promise.allSettled(
16964
+ animations.map((a) => a.finished)
16965
+ ).finally(done);
16966
+ });
16967
+ });
16968
+ });
16969
+ }, maxWaitMs).catch(() => {
16970
+ });
16971
+ await Promise.race([browserWait, nodeTimeout]);
16972
+ } catch {
16973
+ } finally {
16974
+ if (timer) clearTimeout(timer);
16975
+ }
16976
+ }
16882
16977
  async function withPage(opts, driver, fn, target) {
16883
16978
  const state = readBrowseState();
16884
16979
  if (!state) {
@@ -16898,9 +16993,9 @@ async function withPage(opts, driver, fn, target) {
16898
16993
  await fn(page);
16899
16994
  await page.waitForLoadState("domcontentloaded").catch(() => {
16900
16995
  });
16901
- await page.waitForTimeout(400);
16902
16996
  let screenshot = null;
16903
16997
  if (opts.shot) {
16998
+ await waitForVisualStability(page);
16904
16999
  screenshot = path22.resolve(opts.shot);
16905
17000
  mkdirSync(path22.dirname(screenshot), { recursive: true });
16906
17001
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
@@ -18887,7 +18982,7 @@ function composeWalkerPrompt(p) {
18887
18982
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
18888
18983
  `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|observation> --ui-fact "..." --consequence "..." --task-impact <blocks|impairs|slows|neutral|accelerates|enables> --confidence <high|medium|low> --json \u2014 then add the body (description + "## Recommendation") below the frontmatter of the file it returns.`,
18889
18984
  "`--step <N>` is the step whose screenshot shows the problem (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.",
18890
- `Before you finish, write ${rd}/summary.json: {"what_worked": [...], "open_questions": [...]} \u2014 one sentence each, in the persona's language.`,
18985
+ `Before you finish, write ${rd}/summary.json: {"what_worked": [...], "open_questions": [...], "model": "<your model identifier if known, e.g. claude-3-5-haiku, gemini-3.8-flash, or unspecified>"} \u2014 one sentence each, in the persona's language.`,
18891
18986
  "",
18892
18987
  "Do not read the product's source code, other iamu/ entity files, or earlier runs: you know only what the persona knows.",
18893
18988
  "",
@@ -19709,6 +19804,36 @@ async function complete(runId, opts) {
19709
19804
  return;
19710
19805
  }
19711
19806
  const completedAt = pickCompletedAt(run.manifest);
19807
+ const walkerSummary = await readWalkerSummary(run.runDirPath);
19808
+ let judgeReported;
19809
+ try {
19810
+ const verdictsRaw = await fs26.readFile(
19811
+ path26.join(run.runDirPath, "verdicts.json"),
19812
+ "utf8"
19813
+ );
19814
+ const parsed = JSON.parse(verdictsRaw);
19815
+ if (parsed.provenance?.model) {
19816
+ judgeReported = parsed.provenance.model;
19817
+ }
19818
+ } catch {
19819
+ }
19820
+ const stepCount = provenanceAudit.step_count;
19821
+ const durationSec = provenanceAudit.duration_seconds ?? (run.manifest.created_at && Date.parse(completedAt) > Date.parse(run.manifest.created_at) ? Math.round(
19822
+ (Date.parse(completedAt) - Date.parse(run.manifest.created_at)) / 1e3 * 10
19823
+ ) / 10 : null);
19824
+ const velocitySec = provenanceAudit.avg_step_interval_seconds ?? (durationSec != null && stepCount > 1 ? Math.round(durationSec / (stepCount - 1) * 10) / 10 : null);
19825
+ const existingModels = run.manifest.models ?? {};
19826
+ const updatedModels = {
19827
+ ...existingModels,
19828
+ ...walkerSummary.model ? { walker_reported: walkerSummary.model } : {},
19829
+ ...judgeReported ? { judge_reported: judgeReported } : {},
19830
+ ...durationSec != null ? { total_duration_sec: durationSec } : {},
19831
+ ...velocitySec != null ? { velocity_sec_per_step: velocitySec } : {}
19832
+ };
19833
+ run.manifest = {
19834
+ ...run.manifest,
19835
+ models: updatedModels
19836
+ };
19712
19837
  const assembled = await assembleRun(
19713
19838
  run.projectRoot,
19714
19839
  runId,
@@ -19869,6 +19994,17 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
19869
19994
  console.log(` steps ${count(canonical.steps.length)}`);
19870
19995
  console.log(` started ${chalk30.dim(manifest.created_at)}`);
19871
19996
  console.log(` completed ${chalk30.dim(completedAt)}`);
19997
+ const models = manifest.models;
19998
+ if (models?.velocity_sec_per_step != null) {
19999
+ console.log(
20000
+ ` velocity ${chalk30.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk30.dim(`(${models.total_duration_sec}s total)`)}`
20001
+ );
20002
+ }
20003
+ if (models && (models.walker || models.judge || models.matcher)) {
20004
+ const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
20005
+ const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
20006
+ console.log(` models ${chalk30.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
20007
+ }
19872
20008
  console.log();
19873
20009
  console.log(
19874
20010
  ` ${chalk30.dim("canonical:")} ${chalk30.dim(path26.relative(projectRoot, assembled.canonicalPath))}`
@@ -19962,9 +20098,11 @@ async function readWalkerSummary(runDirPath) {
19962
20098
  await fs26.readFile(path26.join(runDirPath, "summary.json"), "utf8")
19963
20099
  );
19964
20100
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
20101
+ const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
19965
20102
  return {
19966
20103
  what_worked: strings(data.what_worked),
19967
- open_questions: strings(data.open_questions)
20104
+ open_questions: strings(data.open_questions),
20105
+ model
19968
20106
  };
19969
20107
  } catch {
19970
20108
  return {};
@@ -20754,6 +20892,15 @@ function registerWalkthroughCommands(program2) {
20754
20892
  ).option(
20755
20893
  "--env <name>",
20756
20894
  "Environment the product runs in (default: the target URL's origin)"
20895
+ ).option(
20896
+ "--walker <tierOrModel>",
20897
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for driving browser navigation (default: fast)"
20898
+ ).option(
20899
+ "--judge <tierOrModel>",
20900
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for human test evaluation (default: thorough)"
20901
+ ).option(
20902
+ "--matcher <tierOrModel>",
20903
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for finding matching (default: thorough)"
20757
20904
  ).option(
20758
20905
  "--check",
20759
20906
  "Enforce target URL reachability (fail fast if server is offline)"
@@ -0,0 +1,52 @@
1
+ import { type BuildMetadata, type RunViewport, type ScenarioFrontmatter } from "@iamu-dev/protocol";
2
+ /**
3
+ * The URL a run starts at: `--target` (or the scenario's `target_url`)
4
+ * as the base, with the scenario's `entry_path` resolved against it.
5
+ * Returns an error message instead when there is nothing usable.
6
+ */
7
+ export declare function resolveStartUrl(target: string | undefined, scenario: Pick<ScenarioFrontmatter, "target_url" | "entry_path">): {
8
+ url: string;
9
+ } | {
10
+ error: string;
11
+ };
12
+ /**
13
+ * Fast preflight probe to check if the target URL answers HTTP requests.
14
+ * Times out quickly (default: 1000ms) to avoid stalling walkthrough initialization.
15
+ */
16
+ export declare function checkTargetReachable(url: string, timeoutMs?: number): Promise<{
17
+ reachable: boolean;
18
+ error?: string;
19
+ }>;
20
+ /**
21
+ * The build as one line: label, branch, commit and CI provider. A dirty
22
+ * tree shows next to the commit, so a re-run after an uncommitted fix
23
+ * visibly walks a different build.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * formatBuild({ git_sha: "90e2444…", git_dirty: true, worktree_hash: "3fa9c1d2e4b5" });
28
+ * // "90e2444 + uncommitted changes (3fa9c1…)"
29
+ * ```
30
+ */
31
+ export declare function formatBuild(build: BuildMetadata): string;
32
+ /**
33
+ * A run viewport as one line.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * formatViewport({ device: "desktop", width: 1280, height: 800, source: "persona-tags" });
38
+ * // "desktop 1280×800 (from persona tags)"
39
+ * ```
40
+ */
41
+ export declare function formatViewport(v: RunViewport): string;
42
+ export interface StartRunOptions {
43
+ target?: string;
44
+ build?: string;
45
+ env?: string;
46
+ json?: boolean;
47
+ check?: boolean;
48
+ walker?: string;
49
+ judge?: string;
50
+ matcher?: string;
51
+ }
52
+ export declare function startRun(scenarioSlug: string, opts: StartRunOptions): Promise<void>;
package/dist/index.js CHANGED
@@ -199,17 +199,24 @@ function defaultConfig(params) {
199
199
  cloud_api_url: null
200
200
  };
201
201
  }
202
- var baseProjectFields, localOnlyConfig, cloudLinkedConfig, projectConfigSchema;
202
+ var modelTierSchema, projectModelsConfigSchema, baseProjectFields, localOnlyConfig, cloudLinkedConfig, projectConfigSchema;
203
203
  var init_config = __esm({
204
204
  "../../packages/protocol/dist/schemas/frontmatter/config.js"() {
205
205
  "use strict";
206
206
  init_common();
207
207
  init_version();
208
+ modelTierSchema = z4.string().min(1).max(128).describe("Model role tier ('fast', 'thorough', 'inherit') or a specific model identifier.");
209
+ projectModelsConfigSchema = z4.object({
210
+ walker: modelTierSchema.optional(),
211
+ matcher: modelTierSchema.optional(),
212
+ judge: modelTierSchema.optional()
213
+ }).strict();
208
214
  baseProjectFields = {
209
215
  schema_version: z4.literal(SCHEMA_VERSION),
210
216
  id: uuidSchema,
211
217
  project_slug: slugSchema,
212
- project_name: z4.string().min(1)
218
+ project_name: z4.string().min(1),
219
+ models: projectModelsConfigSchema.optional()
213
220
  };
214
221
  localOnlyConfig = z4.object({
215
222
  ...baseProjectFields,
@@ -1153,7 +1160,21 @@ var init_walkthrough_bundle = __esm({
1153
1160
  judge_prompt: z19.string().optional(),
1154
1161
  judge_model: z19.string().optional()
1155
1162
  }).optional()
1156
- }).optional()
1163
+ }).optional(),
1164
+ models: z19.object({
1165
+ walker: z19.string().optional(),
1166
+ matcher: z19.string().optional(),
1167
+ judge: z19.string().optional(),
1168
+ walker_reported: z19.string().optional(),
1169
+ matcher_reported: z19.string().optional(),
1170
+ judge_reported: z19.string().optional(),
1171
+ walker_tier: z19.string().optional(),
1172
+ matcher_tier: z19.string().optional(),
1173
+ judge_tier: z19.string().optional(),
1174
+ execution_mode: z19.enum(["multi_agent", "single_agent"]).optional(),
1175
+ velocity_sec_per_step: z19.number().optional(),
1176
+ total_duration_sec: z19.number().optional()
1177
+ }).optional().describe("AI model configuration and reported runtime provenance for this walkthrough.")
1157
1178
  });
1158
1179
  walkthroughBundleSchema = z19.object({
1159
1180
  manifest: bundleManifestSchema,
@@ -1376,6 +1397,7 @@ var init_walkthrough_canonical = __esm({
1376
1397
  build: buildMetadataSchema.optional(),
1377
1398
  viewport: runViewportSchema.optional(),
1378
1399
  environment: z20.string().optional(),
1400
+ models: z20.record(z20.unknown()).optional(),
1379
1401
  findings: z20.array(canonicalFindingSchema),
1380
1402
  steps: z20.array(canonicalStepSchema),
1381
1403
  human_tests: z20.array(canonicalHumanTestSchema),
@@ -7247,6 +7269,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7247
7269
  ...manifest.build ? { build: manifest.build } : {},
7248
7270
  ...manifest.viewport ? { viewport: manifest.viewport } : {},
7249
7271
  ...manifest.environment ? { environment: manifest.environment } : {},
7272
+ ...manifest.models ? { models: manifest.models } : {},
7250
7273
  findings: canonicalFindings,
7251
7274
  steps: canonicalSteps,
7252
7275
  human_tests: humanTests,
@@ -7407,6 +7430,20 @@ function renderCanonicalMarkdown(canonical) {
7407
7430
  lines.push(`- Viewport: ${canonical.viewport.device} (${canonical.viewport.width}\xD7${canonical.viewport.height})`);
7408
7431
  }
7409
7432
  lines.push(`- Started: ${canonical.provenance.started_at}`, `- Completed: ${canonical.provenance.completed_at}`, `- Walker prompt: ${canonical.provenance.walker_prompt.id}@${canonical.provenance.walker_prompt.version}`, `- Findings: ${canonical.findings.length}`, `- Steps: ${canonical.steps.length}`);
7433
+ const models = canonical.models;
7434
+ if (models) {
7435
+ const parts = [
7436
+ models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
7437
+ models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
7438
+ models.matcher ? `Matcher: ${models.matcher}` : ""
7439
+ ].filter(Boolean);
7440
+ if (parts.length > 0) {
7441
+ lines.push(`- Models: ${parts.join(" \xB7 ")}`);
7442
+ }
7443
+ if (typeof models.velocity_sec_per_step === "number") {
7444
+ lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
7445
+ }
7446
+ }
7410
7447
  const what = canonical.what_worked;
7411
7448
  if (what && what.length > 0) {
7412
7449
  lines.push("", "## What worked");
@@ -7776,6 +7813,7 @@ async function assembleWalkthroughBundle(projectRoot, runId) {
7776
7813
  report_markdown: report3,
7777
7814
  verdict: verdictArray,
7778
7815
  judge_meta: judgeMeta,
7816
+ ...manifest.models ? { models: manifest.models } : {},
7779
7817
  ...buildFields(manifest),
7780
7818
  ...await readCanonicalExtras(projectRoot, runId)
7781
7819
  },
@@ -14413,6 +14451,18 @@ async function startRun(scenarioSlug, opts) {
14413
14451
  }
14414
14452
  const build = detectBuild({ cwd: projectRoot, label: opts.build });
14415
14453
  const environment = resolveEnvironment(opts.env, start2.url);
14454
+ const cfg = await readConfig(projectRoot).catch(() => null);
14455
+ const walker = opts.walker ?? process.env.IAMU_WALKER_MODEL ?? cfg?.models?.walker ?? "fast";
14456
+ const judge = opts.judge ?? process.env.IAMU_JUDGE_MODEL ?? cfg?.models?.judge ?? "thorough";
14457
+ const matcher = opts.matcher ?? process.env.IAMU_MATCHER_MODEL ?? cfg?.models?.matcher ?? "thorough";
14458
+ const models = {
14459
+ walker,
14460
+ judge,
14461
+ matcher,
14462
+ walker_tier: ["fast", "thorough", "inherit"].includes(walker) ? walker : "custom",
14463
+ judge_tier: ["fast", "thorough", "inherit"].includes(judge) ? judge : "custom",
14464
+ matcher_tier: ["fast", "thorough", "inherit"].includes(matcher) ? matcher : "custom"
14465
+ };
14416
14466
  let revisit = [];
14417
14467
  try {
14418
14468
  revisit = await computeRevisitPlaces(projectRoot, scenarioSlug, start2.url);
@@ -14430,6 +14480,7 @@ async function startRun(scenarioSlug, opts) {
14430
14480
  status: "running",
14431
14481
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
14432
14482
  completed_at: null,
14483
+ models,
14433
14484
  ...build ? { build } : {},
14434
14485
  ...environment ? { environment } : {},
14435
14486
  ...revisit.length > 0 ? { revisit } : {},
@@ -14451,6 +14502,7 @@ async function startRun(scenarioSlug, opts) {
14451
14502
  ...targetWarning ? { target_warning: targetWarning } : {},
14452
14503
  persona_slug: fm.persona,
14453
14504
  outline_slug: fm.outline,
14505
+ models,
14454
14506
  build: build ?? null,
14455
14507
  environment: environment ?? null,
14456
14508
  revisit,
@@ -14469,6 +14521,9 @@ async function startRun(scenarioSlug, opts) {
14469
14521
  console.log(` ${chalk11.dim("persona:")} ${fm.persona}`);
14470
14522
  console.log(` ${chalk11.dim("outline:")} ${fm.outline}`);
14471
14523
  console.log(` ${chalk11.dim("target:")} ${start2.url}`);
14524
+ console.log(
14525
+ ` ${chalk11.dim("models:")} walker: ${chalk11.cyan(walker)} \xB7 judge: ${chalk11.cyan(judge)} \xB7 matcher: ${chalk11.cyan(matcher)}`
14526
+ );
14472
14527
  if (targetWarning) {
14473
14528
  console.log(` ${symbols.warning} ${chalk11.yellow(targetWarning)}`);
14474
14529
  }
@@ -16652,6 +16707,7 @@ __export(browse_exports, {
16652
16707
  readBrowseState: () => readBrowseState,
16653
16708
  shouldDisableSandbox: () => shouldDisableSandbox,
16654
16709
  start: () => start,
16710
+ waitForVisualStability: () => waitForVisualStability,
16655
16711
  withPage: () => withPage
16656
16712
  });
16657
16713
  import { randomUUID as randomUUID7 } from "crypto";
@@ -16858,6 +16914,45 @@ async function stop(opts, driver) {
16858
16914
  }
16859
16915
  report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
16860
16916
  }
16917
+ async function waitForVisualStability(page, maxWaitMs = 300) {
16918
+ let timer;
16919
+ try {
16920
+ const nodeTimeout = new Promise((resolve3) => {
16921
+ timer = setTimeout(resolve3, maxWaitMs + 100);
16922
+ });
16923
+ const browserWait = page.evaluate((maxWait) => {
16924
+ return new Promise((resolve3) => {
16925
+ const timeout = setTimeout(resolve3, maxWait);
16926
+ const done = () => {
16927
+ clearTimeout(timeout);
16928
+ resolve3();
16929
+ };
16930
+ const g = globalThis;
16931
+ if (typeof g.requestAnimationFrame !== "function") {
16932
+ done();
16933
+ return;
16934
+ }
16935
+ g.requestAnimationFrame(() => {
16936
+ g.requestAnimationFrame?.(() => {
16937
+ const animations = g.document?.getAnimations ? g.document.getAnimations().filter((a) => a.playState === "running" && a.finished) : [];
16938
+ if (animations.length === 0) {
16939
+ done();
16940
+ return;
16941
+ }
16942
+ Promise.allSettled(
16943
+ animations.map((a) => a.finished)
16944
+ ).finally(done);
16945
+ });
16946
+ });
16947
+ });
16948
+ }, maxWaitMs).catch(() => {
16949
+ });
16950
+ await Promise.race([browserWait, nodeTimeout]);
16951
+ } catch {
16952
+ } finally {
16953
+ if (timer) clearTimeout(timer);
16954
+ }
16955
+ }
16861
16956
  async function withPage(opts, driver, fn, target) {
16862
16957
  const state = readBrowseState();
16863
16958
  if (!state) {
@@ -16877,9 +16972,9 @@ async function withPage(opts, driver, fn, target) {
16877
16972
  await fn(page);
16878
16973
  await page.waitForLoadState("domcontentloaded").catch(() => {
16879
16974
  });
16880
- await page.waitForTimeout(400);
16881
16975
  let screenshot = null;
16882
16976
  if (opts.shot) {
16977
+ await waitForVisualStability(page);
16883
16978
  screenshot = path22.resolve(opts.shot);
16884
16979
  mkdirSync(path22.dirname(screenshot), { recursive: true });
16885
16980
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
@@ -18866,7 +18961,7 @@ function composeWalkerPrompt(p) {
18866
18961
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
18867
18962
  `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|observation> --ui-fact "..." --consequence "..." --task-impact <blocks|impairs|slows|neutral|accelerates|enables> --confidence <high|medium|low> --json \u2014 then add the body (description + "## Recommendation") below the frontmatter of the file it returns.`,
18868
18963
  "`--step <N>` is the step whose screenshot shows the problem (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.",
18869
- `Before you finish, write ${rd}/summary.json: {"what_worked": [...], "open_questions": [...]} \u2014 one sentence each, in the persona's language.`,
18964
+ `Before you finish, write ${rd}/summary.json: {"what_worked": [...], "open_questions": [...], "model": "<your model identifier if known, e.g. claude-3-5-haiku, gemini-3.8-flash, or unspecified>"} \u2014 one sentence each, in the persona's language.`,
18870
18965
  "",
18871
18966
  "Do not read the product's source code, other iamu/ entity files, or earlier runs: you know only what the persona knows.",
18872
18967
  "",
@@ -19688,6 +19783,36 @@ async function complete(runId, opts) {
19688
19783
  return;
19689
19784
  }
19690
19785
  const completedAt = pickCompletedAt(run.manifest);
19786
+ const walkerSummary = await readWalkerSummary(run.runDirPath);
19787
+ let judgeReported;
19788
+ try {
19789
+ const verdictsRaw = await fs26.readFile(
19790
+ path26.join(run.runDirPath, "verdicts.json"),
19791
+ "utf8"
19792
+ );
19793
+ const parsed = JSON.parse(verdictsRaw);
19794
+ if (parsed.provenance?.model) {
19795
+ judgeReported = parsed.provenance.model;
19796
+ }
19797
+ } catch {
19798
+ }
19799
+ const stepCount = provenanceAudit.step_count;
19800
+ const durationSec = provenanceAudit.duration_seconds ?? (run.manifest.created_at && Date.parse(completedAt) > Date.parse(run.manifest.created_at) ? Math.round(
19801
+ (Date.parse(completedAt) - Date.parse(run.manifest.created_at)) / 1e3 * 10
19802
+ ) / 10 : null);
19803
+ const velocitySec = provenanceAudit.avg_step_interval_seconds ?? (durationSec != null && stepCount > 1 ? Math.round(durationSec / (stepCount - 1) * 10) / 10 : null);
19804
+ const existingModels = run.manifest.models ?? {};
19805
+ const updatedModels = {
19806
+ ...existingModels,
19807
+ ...walkerSummary.model ? { walker_reported: walkerSummary.model } : {},
19808
+ ...judgeReported ? { judge_reported: judgeReported } : {},
19809
+ ...durationSec != null ? { total_duration_sec: durationSec } : {},
19810
+ ...velocitySec != null ? { velocity_sec_per_step: velocitySec } : {}
19811
+ };
19812
+ run.manifest = {
19813
+ ...run.manifest,
19814
+ models: updatedModels
19815
+ };
19691
19816
  const assembled = await assembleRun(
19692
19817
  run.projectRoot,
19693
19818
  runId,
@@ -19848,6 +19973,17 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
19848
19973
  console.log(` steps ${count(canonical.steps.length)}`);
19849
19974
  console.log(` started ${chalk30.dim(manifest.created_at)}`);
19850
19975
  console.log(` completed ${chalk30.dim(completedAt)}`);
19976
+ const models = manifest.models;
19977
+ if (models?.velocity_sec_per_step != null) {
19978
+ console.log(
19979
+ ` velocity ${chalk30.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk30.dim(`(${models.total_duration_sec}s total)`)}`
19980
+ );
19981
+ }
19982
+ if (models && (models.walker || models.judge || models.matcher)) {
19983
+ const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
19984
+ const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
19985
+ console.log(` models ${chalk30.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
19986
+ }
19851
19987
  console.log();
19852
19988
  console.log(
19853
19989
  ` ${chalk30.dim("canonical:")} ${chalk30.dim(path26.relative(projectRoot, assembled.canonicalPath))}`
@@ -19941,9 +20077,11 @@ async function readWalkerSummary(runDirPath) {
19941
20077
  await fs26.readFile(path26.join(runDirPath, "summary.json"), "utf8")
19942
20078
  );
19943
20079
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
20080
+ const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
19944
20081
  return {
19945
20082
  what_worked: strings(data.what_worked),
19946
- open_questions: strings(data.open_questions)
20083
+ open_questions: strings(data.open_questions),
20084
+ model
19947
20085
  };
19948
20086
  } catch {
19949
20087
  return {};
@@ -20725,6 +20863,15 @@ function registerWalkthroughCommands(program) {
20725
20863
  ).option(
20726
20864
  "--env <name>",
20727
20865
  "Environment the product runs in (default: the target URL's origin)"
20866
+ ).option(
20867
+ "--walker <tierOrModel>",
20868
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for driving browser navigation (default: fast)"
20869
+ ).option(
20870
+ "--judge <tierOrModel>",
20871
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for human test evaluation (default: thorough)"
20872
+ ).option(
20873
+ "--matcher <tierOrModel>",
20874
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for finding matching (default: thorough)"
20728
20875
  ).option(
20729
20876
  "--check",
20730
20877
  "Enforce target URL reachability (fail fast if server is offline)"
@@ -185,12 +185,16 @@ The result's `walker_prompt` is the walker's entire prompt. Pass it verbatim —
185
185
 
186
186
  A fresh subagent is NOT blind by default. On most hosts it still loads the project's agent instructions and can read files in the repo, so it may know the domain model, the route table, and why a screen was built the way it was — none of which the persona knows. The methodology's "Stay outside the product" rule is what keeps it honest; it is part of `walker_prompt` — never remove or soften it. If the host lets you restrict a subagent's tools, give the walker only the driving surface, file write/edit, and a shell for `iamu` commands.
187
187
 
188
+ **Model tiering & subagent dispatch:**
189
+ - **Walker (Browser Navigation):** High turn count, mechanical tool-use. Use the **fast** model tier (`flash` or `haiku`) by default to cut walk time by ~60% and conserve session quota.
190
+ - **Matcher & Judge:** Broad synthesis and semantic comparison. Use the **thorough** frontier tier (`pro`, `sonnet`, or active model) to prevent severity inflation.
191
+
188
192
  ```
189
193
  # In Claude Code:
190
- Agent({ description: "Walk <scenario-slug> as <persona.name>", prompt: "<walker_prompt, verbatim>" })
194
+ Agent({ description: "Walk <scenario-slug> as <persona.name>", model: "haiku", prompt: "<walker_prompt, verbatim>" })
191
195
 
192
196
  # In Antigravity / generic subagent hosts:
193
- invoke_subagent(Role="Persona Walker", Prompt="<walker_prompt, verbatim>")
197
+ invoke_subagent(Role="Persona Walker", Model="flash", Prompt="<walker_prompt, verbatim>")
194
198
  ```
195
199
 
196
200
  The subagent inherits the driving-surface tools and drives the product independently. It does not see this conversation.
@@ -202,7 +206,7 @@ The subagent inherits the driving-surface tools and drives the product independe
202
206
  - **Screenshot every 3-4 taps at most.** Batching more desyncs the moment an answer opens a screen she did not expect.
203
207
  - **Coordinates are device points, origin top-left.** A scaled screenshot must be mapped back to that space before tapping.
204
208
 
205
- **If subagent dispatch is not supported by your host editor** (e.g. standard Cursor without subagent tools), fall back to walking in the same context — but strictly adhere to the persona behavioral constraints in `walker_prompt`.
209
+ **If subagent dispatch or model-switching is not supported by your host editor** (e.g. standard Cursor / Windsurf single-session mode), fall back to walking directly in the current session (inheriting the active model) — but strictly adhere to the persona behavioral constraints in `walker_prompt`.
206
210
 
207
211
  After the walker returns, continue to the post-walk pipeline below.
208
212
 
@@ -269,7 +273,7 @@ This reads all steps, findings, and entity files, assembles `canonical.json`, ge
269
273
  iamu match <run-id> --json
270
274
  ```
271
275
 
272
- If `needed` is `false` (no earlier findings for this scenario, or none this run), skip to C. Otherwise, read the prompt from `iamu/.runs/<run-id>/match_prompt.md` directly and dispatch a matcher subagent with it as its only context. Write its JSON answer to `<matches.json>` and record it:
276
+ If `needed` is `false` (no earlier findings for this scenario, or none this run), skip to C. Otherwise, read the prompt from `iamu/.runs/<run-id>/match_prompt.md` directly and dispatch a matcher subagent with it as its only context (use the **thorough** model tier, e.g. `Model="pro"` in Antigravity, to accurately evaluate semantic equivalence). Write its JSON answer to `<matches.json>` and record it:
273
277
 
274
278
  ```bash
275
279
  iamu match-record <run-id> --file <matches.json>
@@ -283,7 +287,7 @@ This links recurrences to their earlier canonical finding, marks earlier finding
283
287
  iamu evaluate <run-id> --json
284
288
  ```
285
289
 
286
- Read the evaluation prompt from `iamu/.runs/<run-id>/judge_prompt.md` directly. Dispatch the evaluator as a subagent with that prompt. After the evaluator returns verdicts, record them locally with `--no-push`:
290
+ Read the evaluation prompt from `iamu/.runs/<run-id>/judge_prompt.md` directly. Dispatch the evaluator as a subagent with that prompt (use the **thorough** model tier, e.g. `Model="pro"` in Antigravity, to prevent severity inflation). After the evaluator returns verdicts, record them locally with `--no-push`:
287
291
 
288
292
  ```bash
289
293
  iamu evaluate-record <run-id> --file <verdicts.json> --no-prompt --no-push
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamu-dev/iamu",
3
- "version": "0.2.0-beta.5",
3
+ "version": "0.2.0-beta.6",
4
4
  "type": "module",
5
5
  "description": "Test your product as realistic personas — scenario-driven development CLI",
6
6
  "license": "UNLICENSED",
@@ -57,9 +57,9 @@
57
57
  "tsup": "^8.5.1",
58
58
  "typescript": "^5.8.0",
59
59
  "vitest": "^4.1.11",
60
+ "@iamu-dev/protocol": "0.1.0",
60
61
  "@iamu-dev/local-store": "0.1.0",
61
- "@iamu/tsconfig": "0.1.0",
62
- "@iamu-dev/protocol": "0.1.0"
62
+ "@iamu/tsconfig": "0.1.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsup && tsc -b tsconfig.build.json --emitDeclarationOnly && node scripts/copy-assets.mjs",