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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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,
@@ -816,7 +823,14 @@ var init_scenario = __esm({
816
823
  reviews: z15.array(z15.string().min(1).max(64)).max(16).default([]),
817
824
  baseline_score: z15.number().min(0).max(100).nullish(),
818
825
  current_score: z15.number().min(0).max(100).nullish(),
819
- target_score: z15.number().min(0).max(100).default(95)
826
+ target_score: z15.number().min(0).max(100).default(95),
827
+ /** Environment state preconditions and approximation guidance. */
828
+ state: z15.object({
829
+ /** Description of data/records expected to exist in the environment. */
830
+ description: z15.string().max(2048).nullish(),
831
+ /** Guidance on how to squint or approximate if exact records are absent. */
832
+ approximation: z15.string().max(2048).nullish()
833
+ }).nullish()
820
834
  });
821
835
  }
822
836
  });
@@ -1153,7 +1167,21 @@ var init_walkthrough_bundle = __esm({
1153
1167
  judge_prompt: z19.string().optional(),
1154
1168
  judge_model: z19.string().optional()
1155
1169
  }).optional()
1156
- }).optional()
1170
+ }).optional(),
1171
+ models: z19.object({
1172
+ walker: z19.string().optional(),
1173
+ matcher: z19.string().optional(),
1174
+ judge: z19.string().optional(),
1175
+ walker_reported: z19.string().optional(),
1176
+ matcher_reported: z19.string().optional(),
1177
+ judge_reported: z19.string().optional(),
1178
+ walker_tier: z19.string().optional(),
1179
+ matcher_tier: z19.string().optional(),
1180
+ judge_tier: z19.string().optional(),
1181
+ execution_mode: z19.enum(["multi_agent", "single_agent"]).optional(),
1182
+ velocity_sec_per_step: z19.number().optional(),
1183
+ total_duration_sec: z19.number().optional()
1184
+ }).optional().describe("AI model configuration and reported runtime provenance for this walkthrough.")
1157
1185
  });
1158
1186
  walkthroughBundleSchema = z19.object({
1159
1187
  manifest: bundleManifestSchema,
@@ -1376,6 +1404,7 @@ var init_walkthrough_canonical = __esm({
1376
1404
  build: buildMetadataSchema.optional(),
1377
1405
  viewport: runViewportSchema.optional(),
1378
1406
  environment: z20.string().optional(),
1407
+ models: z20.record(z20.unknown()).optional(),
1379
1408
  findings: z20.array(canonicalFindingSchema),
1380
1409
  steps: z20.array(canonicalStepSchema),
1381
1410
  human_tests: z20.array(canonicalHumanTestSchema),
@@ -7247,6 +7276,7 @@ async function buildCanonical(projectRoot, manifest, completedAt, inputs) {
7247
7276
  ...manifest.build ? { build: manifest.build } : {},
7248
7277
  ...manifest.viewport ? { viewport: manifest.viewport } : {},
7249
7278
  ...manifest.environment ? { environment: manifest.environment } : {},
7279
+ ...manifest.models ? { models: manifest.models } : {},
7250
7280
  findings: canonicalFindings,
7251
7281
  steps: canonicalSteps,
7252
7282
  human_tests: humanTests,
@@ -7407,6 +7437,20 @@ function renderCanonicalMarkdown(canonical) {
7407
7437
  lines.push(`- Viewport: ${canonical.viewport.device} (${canonical.viewport.width}\xD7${canonical.viewport.height})`);
7408
7438
  }
7409
7439
  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}`);
7440
+ const models = canonical.models;
7441
+ if (models) {
7442
+ const parts = [
7443
+ models.walker ? `Walker: ${models.walker}${models.walker_reported ? ` (${models.walker_reported})` : ""}` : "",
7444
+ models.judge ? `Judge: ${models.judge}${models.judge_reported ? ` (${models.judge_reported})` : ""}` : "",
7445
+ models.matcher ? `Matcher: ${models.matcher}` : ""
7446
+ ].filter(Boolean);
7447
+ if (parts.length > 0) {
7448
+ lines.push(`- Models: ${parts.join(" \xB7 ")}`);
7449
+ }
7450
+ if (typeof models.velocity_sec_per_step === "number") {
7451
+ lines.push(`- Velocity: ~${models.velocity_sec_per_step}s/step (${models.total_duration_sec}s total)`);
7452
+ }
7453
+ }
7410
7454
  const what = canonical.what_worked;
7411
7455
  if (what && what.length > 0) {
7412
7456
  lines.push("", "## What worked");
@@ -7776,6 +7820,7 @@ async function assembleWalkthroughBundle(projectRoot, runId) {
7776
7820
  report_markdown: report3,
7777
7821
  verdict: verdictArray,
7778
7822
  judge_meta: judgeMeta,
7823
+ ...manifest.models ? { models: manifest.models } : {},
7779
7824
  ...buildFields(manifest),
7780
7825
  ...await readCanonicalExtras(projectRoot, runId)
7781
7826
  },
@@ -12266,15 +12311,6 @@ function mcpJson(cmd) {
12266
12311
  2
12267
12312
  );
12268
12313
  }
12269
- function vscodeMcpJson(cmd) {
12270
- return JSON.stringify(
12271
- {
12272
- servers: { iamu: { type: "stdio", command: cmd[0], args: cmd.slice(1) } }
12273
- },
12274
- null,
12275
- 2
12276
- );
12277
- }
12278
12314
  function detectHosts(projectRoot) {
12279
12315
  return HOST_ADAPTERS.filter(
12280
12316
  (host) => host.detectionPaths.some((p) => existsSync3(join3(projectRoot, p)))
@@ -12323,13 +12359,17 @@ var init_registry = __esm({
12323
12359
  {
12324
12360
  name: "GitHub Copilot",
12325
12361
  value: "github-copilot",
12326
- detectionPaths: [".vscode", ".github/copilot-instructions.md"],
12327
- supportsSkills: false,
12328
- getSkillsDir: () => "",
12329
- getCommandPath: (root, id) => join3(root, ".github", "prompts", `iamu-${id}.prompt.md`),
12330
- formatCommand: descOnlyFormat,
12331
- getMcpConfigPath: (root) => join3(root, ".vscode", "mcp.json"),
12332
- formatMcpConfig: vscodeMcpJson
12362
+ detectionPaths: [
12363
+ ".github/copilot-instructions.md",
12364
+ ".github/skills",
12365
+ ".copilot"
12366
+ ],
12367
+ supportsSkills: true,
12368
+ getSkillsDir: (root) => join3(root, ".github", "skills"),
12369
+ getCommandPath: (root, id) => join3(root, ".github", "skills", `iamu-${id}`, "SKILL.md"),
12370
+ formatCommand: richFormat,
12371
+ getMcpConfigPath: (root) => join3(root, ".mcp.json"),
12372
+ formatMcpConfig: mcpJson
12333
12373
  },
12334
12374
  {
12335
12375
  name: "Cline",
@@ -12511,6 +12551,14 @@ function isCliWrittenSkill(path28) {
12511
12551
  return false;
12512
12552
  }
12513
12553
  }
12554
+ function isCliWrittenPrompt(path28) {
12555
+ try {
12556
+ const content = readFileSync3(path28, "utf-8");
12557
+ return content.includes("iamu") || /^generatedBy:/m.test(content);
12558
+ } catch {
12559
+ return false;
12560
+ }
12561
+ }
12514
12562
  function planSkillFiles(hosts, projectRoot, version) {
12515
12563
  const skillTemplates = loadSkillTemplates(version);
12516
12564
  const files = [];
@@ -12539,6 +12587,19 @@ function planSkillFiles(hosts, projectRoot, version) {
12539
12587
  }
12540
12588
  }
12541
12589
  }
12590
+ if (host.value === "github-copilot") {
12591
+ for (const cmd of commandsFromSkills(skillTemplates)) {
12592
+ const legacyPrompt = join4(
12593
+ projectRoot,
12594
+ ".github",
12595
+ "prompts",
12596
+ `iamu-${cmd.id}.prompt.md`
12597
+ );
12598
+ if (isCliWrittenPrompt(legacyPrompt)) {
12599
+ files.push({ host, path: legacyPrompt, content: null });
12600
+ }
12601
+ }
12602
+ }
12542
12603
  } else {
12543
12604
  for (const cmd of commandsFromSkills(skillTemplates)) {
12544
12605
  files.push({
@@ -12563,6 +12624,15 @@ function detectInstalledHosts(projectRoot) {
12563
12624
  const skillDir = join4(host.getSkillsDir(projectRoot), "iamu-setup");
12564
12625
  if (existsSync5(skillDir)) return true;
12565
12626
  }
12627
+ if (host.value === "github-copilot") {
12628
+ const legacyPrompt = join4(
12629
+ projectRoot,
12630
+ ".github",
12631
+ "prompts",
12632
+ "iamu-setup.prompt.md"
12633
+ );
12634
+ if (existsSync5(legacyPrompt)) return true;
12635
+ }
12566
12636
  return false;
12567
12637
  });
12568
12638
  }
@@ -14413,6 +14483,18 @@ async function startRun(scenarioSlug, opts) {
14413
14483
  }
14414
14484
  const build = detectBuild({ cwd: projectRoot, label: opts.build });
14415
14485
  const environment = resolveEnvironment(opts.env, start2.url);
14486
+ const cfg = await readConfig(projectRoot).catch(() => null);
14487
+ const walker = opts.walker ?? process.env.IAMU_WALKER_MODEL ?? cfg?.models?.walker ?? "fast";
14488
+ const judge = opts.judge ?? process.env.IAMU_JUDGE_MODEL ?? cfg?.models?.judge ?? "thorough";
14489
+ const matcher = opts.matcher ?? process.env.IAMU_MATCHER_MODEL ?? cfg?.models?.matcher ?? "thorough";
14490
+ const models = {
14491
+ walker,
14492
+ judge,
14493
+ matcher,
14494
+ walker_tier: ["fast", "thorough", "inherit"].includes(walker) ? walker : "custom",
14495
+ judge_tier: ["fast", "thorough", "inherit"].includes(judge) ? judge : "custom",
14496
+ matcher_tier: ["fast", "thorough", "inherit"].includes(matcher) ? matcher : "custom"
14497
+ };
14416
14498
  let revisit = [];
14417
14499
  try {
14418
14500
  revisit = await computeRevisitPlaces(projectRoot, scenarioSlug, start2.url);
@@ -14430,6 +14512,7 @@ async function startRun(scenarioSlug, opts) {
14430
14512
  status: "running",
14431
14513
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
14432
14514
  completed_at: null,
14515
+ models,
14433
14516
  ...build ? { build } : {},
14434
14517
  ...environment ? { environment } : {},
14435
14518
  ...revisit.length > 0 ? { revisit } : {},
@@ -14451,6 +14534,7 @@ async function startRun(scenarioSlug, opts) {
14451
14534
  ...targetWarning ? { target_warning: targetWarning } : {},
14452
14535
  persona_slug: fm.persona,
14453
14536
  outline_slug: fm.outline,
14537
+ models,
14454
14538
  build: build ?? null,
14455
14539
  environment: environment ?? null,
14456
14540
  revisit,
@@ -14469,6 +14553,9 @@ async function startRun(scenarioSlug, opts) {
14469
14553
  console.log(` ${chalk11.dim("persona:")} ${fm.persona}`);
14470
14554
  console.log(` ${chalk11.dim("outline:")} ${fm.outline}`);
14471
14555
  console.log(` ${chalk11.dim("target:")} ${start2.url}`);
14556
+ console.log(
14557
+ ` ${chalk11.dim("models:")} walker: ${chalk11.cyan(walker)} \xB7 judge: ${chalk11.cyan(judge)} \xB7 matcher: ${chalk11.cyan(matcher)}`
14558
+ );
14472
14559
  if (targetWarning) {
14473
14560
  console.log(` ${symbols.warning} ${chalk11.yellow(targetWarning)}`);
14474
14561
  }
@@ -16265,7 +16352,10 @@ var init_browse_actions = __esm({
16265
16352
  usage: "goto <url>",
16266
16353
  minArgs: 1,
16267
16354
  run: async (p, args) => {
16268
- await p.goto(args[0], { waitUntil: "domcontentloaded" });
16355
+ await p.goto(args[0], {
16356
+ waitUntil: "domcontentloaded",
16357
+ timeout: 1e4
16358
+ });
16269
16359
  }
16270
16360
  },
16271
16361
  click: {
@@ -16426,6 +16516,12 @@ var init_browse_driver = __esm({
16426
16516
  commandLine: (pid) => readProcessCommandLine(pid),
16427
16517
  kill(pid) {
16428
16518
  try {
16519
+ if (process.platform !== "win32") {
16520
+ try {
16521
+ process.kill(-pid, "SIGTERM");
16522
+ } catch {
16523
+ }
16524
+ }
16429
16525
  process.kill(pid, "SIGTERM");
16430
16526
  } catch {
16431
16527
  }
@@ -16652,6 +16748,7 @@ __export(browse_exports, {
16652
16748
  readBrowseState: () => readBrowseState,
16653
16749
  shouldDisableSandbox: () => shouldDisableSandbox,
16654
16750
  start: () => start,
16751
+ waitForVisualStability: () => waitForVisualStability,
16655
16752
  withPage: () => withPage
16656
16753
  });
16657
16754
  import { randomUUID as randomUUID7 } from "crypto";
@@ -16801,6 +16898,8 @@ function chromeArgs(opts, port, headless) {
16801
16898
  `--window-size=${opts.width ?? "1280"},${opts.height ?? "900"}`,
16802
16899
  "--no-first-run",
16803
16900
  "--no-default-browser-check",
16901
+ "--password-store=basic",
16902
+ "--use-mock-keychain",
16804
16903
  ...headless ? ["--headless=new", "--disable-dev-shm-usage", "--disable-gpu"] : [],
16805
16904
  ...shouldDisableSandbox() ? ["--no-sandbox"] : [],
16806
16905
  "about:blank"
@@ -16858,6 +16957,45 @@ async function stop(opts, driver) {
16858
16957
  }
16859
16958
  report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
16860
16959
  }
16960
+ async function waitForVisualStability(page, maxWaitMs = 300) {
16961
+ let timer;
16962
+ try {
16963
+ const nodeTimeout = new Promise((resolve3) => {
16964
+ timer = setTimeout(resolve3, maxWaitMs + 100);
16965
+ });
16966
+ const browserWait = page.evaluate((maxWait) => {
16967
+ return new Promise((resolve3) => {
16968
+ const timeout = setTimeout(resolve3, maxWait);
16969
+ const done = () => {
16970
+ clearTimeout(timeout);
16971
+ resolve3();
16972
+ };
16973
+ const g = globalThis;
16974
+ if (typeof g.requestAnimationFrame !== "function") {
16975
+ done();
16976
+ return;
16977
+ }
16978
+ g.requestAnimationFrame(() => {
16979
+ g.requestAnimationFrame?.(() => {
16980
+ const animations = g.document?.getAnimations ? g.document.getAnimations().filter((a) => a.playState === "running" && a.finished) : [];
16981
+ if (animations.length === 0) {
16982
+ done();
16983
+ return;
16984
+ }
16985
+ Promise.allSettled(
16986
+ animations.map((a) => a.finished)
16987
+ ).finally(done);
16988
+ });
16989
+ });
16990
+ });
16991
+ }, maxWaitMs).catch(() => {
16992
+ });
16993
+ await Promise.race([browserWait, nodeTimeout]);
16994
+ } catch {
16995
+ } finally {
16996
+ if (timer) clearTimeout(timer);
16997
+ }
16998
+ }
16861
16999
  async function withPage(opts, driver, fn, target) {
16862
17000
  const state = readBrowseState();
16863
17001
  if (!state) {
@@ -16877,9 +17015,9 @@ async function withPage(opts, driver, fn, target) {
16877
17015
  await fn(page);
16878
17016
  await page.waitForLoadState("domcontentloaded").catch(() => {
16879
17017
  });
16880
- await page.waitForTimeout(400);
16881
17018
  let screenshot = null;
16882
17019
  if (opts.shot) {
17020
+ await waitForVisualStability(page);
16883
17021
  screenshot = path22.resolve(opts.shot);
16884
17022
  mkdirSync(path22.dirname(screenshot), { recursive: true });
16885
17023
  await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
@@ -18438,13 +18576,21 @@ function defaultFrontmatter(type2, id, slug, name, opts) {
18438
18576
  url: opts.url,
18439
18577
  authority_level: opts.authority ?? "informational"
18440
18578
  };
18441
- case "scenario":
18579
+ case "scenario": {
18580
+ const stateObj = opts.stateDescription || opts.stateApproximation ? {
18581
+ state: {
18582
+ ...opts.stateDescription ? { description: opts.stateDescription } : {},
18583
+ ...opts.stateApproximation ? { approximation: opts.stateApproximation } : {}
18584
+ }
18585
+ } : {};
18442
18586
  return {
18443
18587
  ...base,
18444
18588
  outline: opts.outline ?? null,
18445
18589
  persona: opts.persona ?? null,
18446
- ...opts.entry ? { entry_path: opts.entry } : {}
18590
+ ...opts.entry ? { entry_path: opts.entry } : {},
18591
+ ...stateObj
18447
18592
  };
18593
+ }
18448
18594
  default:
18449
18595
  return base;
18450
18596
  }
@@ -18838,6 +18984,17 @@ function composeWalkerPrompt(p) {
18838
18984
  "",
18839
18985
  "## First-person brief",
18840
18986
  p.brief ?? "",
18987
+ ...p.context ? ["", "## Product domain & regulatory context", p.context.trim()] : [],
18988
+ ...p.state?.description || p.state?.approximation ? [
18989
+ "",
18990
+ "## Expected state & semantic squinting",
18991
+ p.state.description ? `Expected state: ${p.state.description}` : "",
18992
+ p.state.approximation ? `Approximation strategy: ${p.state.approximation}` : "",
18993
+ "Squinting Guardrails:",
18994
+ "\u2022 If the exact target record/option is not available, apply the approximation strategy above to pick the closest plausible entity within the domain flow.",
18995
+ "\u2022 Squinting does NOT permit drifting to unrelated settings, account profiles, or off-target pages. Stay strictly within the scenario journey.",
18996
+ "\u2022 If the system is completely empty or unseeded, and your persona role cannot create data, do NOT report false product bugs about the empty state. Note that preconditions are unseeded/blocked."
18997
+ ].filter(Boolean) : [],
18841
18998
  ...revisitSection(p.revisit ?? []),
18842
18999
  "",
18843
19000
  "## Access",
@@ -18866,7 +19023,7 @@ function composeWalkerPrompt(p) {
18866
19023
  `Screenshots to: ${rd}/screenshots/step-N.png \u2014 absolute paths.`,
18867
19024
  `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
19025
  "`--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.`,
19026
+ `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
19027
  "",
18871
19028
  "Do not read the product's source code, other iamu/ entity files, or earlier runs: you know only what the persona knows.",
18872
19029
  "",
@@ -18986,7 +19143,8 @@ async function instructionsWalk(opts) {
18986
19143
  const result = {
18987
19144
  scenario: {
18988
19145
  slug: sfm.slug,
18989
- brief: scenario.body.trim() || null
19146
+ brief: scenario.body.trim() || null,
19147
+ state: sfm.state ?? null
18990
19148
  },
18991
19149
  persona: {
18992
19150
  slug: sfm.persona,
@@ -19040,7 +19198,9 @@ async function instructionsWalk(opts) {
19040
19198
  runDirPath,
19041
19199
  projectRoot,
19042
19200
  surface: opts.surface ?? "browse",
19043
- viewport
19201
+ viewport,
19202
+ state: sfm.state ?? null,
19203
+ context: contextYml
19044
19204
  }) : null
19045
19205
  };
19046
19206
  if (opts.json) {
@@ -19052,6 +19212,11 @@ async function instructionsWalk(opts) {
19052
19212
  console.log(` ${chalk26.dim("scenario:")} ${sfm.slug}`);
19053
19213
  console.log(` ${chalk26.dim("persona:")} ${pfm.name} (${pfm.role})`);
19054
19214
  console.log(` ${chalk26.dim("outline:")} ${ofm.title}`);
19215
+ if (sfm.state?.description || sfm.state?.approximation) {
19216
+ console.log(
19217
+ ` ${chalk26.dim("state:")} ${sfm.state.description ? sfm.state.description : sfm.state.approximation ?? ""}`
19218
+ );
19219
+ }
19055
19220
  if (targetUrl) console.log(` ${chalk26.dim("target:")} ${targetUrl}`);
19056
19221
  console.log(
19057
19222
  ` ${chalk26.dim("access:")} ${projectAccess || personaAccess ? `${projectAccess ? "project" : ""}${projectAccess && personaAccess ? " + " : ""}${personaAccess ? "persona" : ""}` : "not recorded (walker would run as whoever is signed in)"}`
@@ -19688,6 +19853,36 @@ async function complete(runId, opts) {
19688
19853
  return;
19689
19854
  }
19690
19855
  const completedAt = pickCompletedAt(run.manifest);
19856
+ const walkerSummary = await readWalkerSummary(run.runDirPath);
19857
+ let judgeReported;
19858
+ try {
19859
+ const verdictsRaw = await fs26.readFile(
19860
+ path26.join(run.runDirPath, "verdicts.json"),
19861
+ "utf8"
19862
+ );
19863
+ const parsed = JSON.parse(verdictsRaw);
19864
+ if (parsed.provenance?.model) {
19865
+ judgeReported = parsed.provenance.model;
19866
+ }
19867
+ } catch {
19868
+ }
19869
+ const stepCount = provenanceAudit.step_count;
19870
+ const durationSec = provenanceAudit.duration_seconds ?? (run.manifest.created_at && Date.parse(completedAt) > Date.parse(run.manifest.created_at) ? Math.round(
19871
+ (Date.parse(completedAt) - Date.parse(run.manifest.created_at)) / 1e3 * 10
19872
+ ) / 10 : null);
19873
+ const velocitySec = provenanceAudit.avg_step_interval_seconds ?? (durationSec != null && stepCount > 1 ? Math.round(durationSec / (stepCount - 1) * 10) / 10 : null);
19874
+ const existingModels = run.manifest.models ?? {};
19875
+ const updatedModels = {
19876
+ ...existingModels,
19877
+ ...walkerSummary.model ? { walker_reported: walkerSummary.model } : {},
19878
+ ...judgeReported ? { judge_reported: judgeReported } : {},
19879
+ ...durationSec != null ? { total_duration_sec: durationSec } : {},
19880
+ ...velocitySec != null ? { velocity_sec_per_step: velocitySec } : {}
19881
+ };
19882
+ run.manifest = {
19883
+ ...run.manifest,
19884
+ models: updatedModels
19885
+ };
19691
19886
  const assembled = await assembleRun(
19692
19887
  run.projectRoot,
19693
19888
  runId,
@@ -19848,6 +20043,17 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
19848
20043
  console.log(` steps ${count(canonical.steps.length)}`);
19849
20044
  console.log(` started ${chalk30.dim(manifest.created_at)}`);
19850
20045
  console.log(` completed ${chalk30.dim(completedAt)}`);
20046
+ const models = manifest.models;
20047
+ if (models?.velocity_sec_per_step != null) {
20048
+ console.log(
20049
+ ` velocity ${chalk30.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk30.dim(`(${models.total_duration_sec}s total)`)}`
20050
+ );
20051
+ }
20052
+ if (models && (models.walker || models.judge || models.matcher)) {
20053
+ const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
20054
+ const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
20055
+ console.log(` models ${chalk30.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
20056
+ }
19851
20057
  console.log();
19852
20058
  console.log(
19853
20059
  ` ${chalk30.dim("canonical:")} ${chalk30.dim(path26.relative(projectRoot, assembled.canonicalPath))}`
@@ -19941,9 +20147,11 @@ async function readWalkerSummary(runDirPath) {
19941
20147
  await fs26.readFile(path26.join(runDirPath, "summary.json"), "utf8")
19942
20148
  );
19943
20149
  const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
20150
+ const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
19944
20151
  return {
19945
20152
  what_worked: strings(data.what_worked),
19946
- open_questions: strings(data.open_questions)
20153
+ open_questions: strings(data.open_questions),
20154
+ model
19947
20155
  };
19948
20156
  } catch {
19949
20157
  return {};
@@ -20684,6 +20892,12 @@ function registerProjectCommands(program) {
20684
20892
  ).option("--outline <slug>", "Outline slug (scenario only)").option("--persona <slug>", "Persona slug (scenario only)").option(
20685
20893
  "--entry <path>",
20686
20894
  "Where the situation starts, as a path on the product, e.g. /en/episodes/1 (scenario only)"
20895
+ ).option(
20896
+ "--state-description <text>",
20897
+ "Assumed environment data/records state (scenario only)"
20898
+ ).option(
20899
+ "--state-approximation <text>",
20900
+ "Guidance on how persona should approximate/squint if records are missing (scenario only)"
20687
20901
  ).option("--url <url>", "Document the source cites (source only, required)").option(
20688
20902
  "--authority <level>",
20689
20903
  "legislation | regulation | standard | company_policy | guideline | informational (source only, default informational)"
@@ -20725,6 +20939,15 @@ function registerWalkthroughCommands(program) {
20725
20939
  ).option(
20726
20940
  "--env <name>",
20727
20941
  "Environment the product runs in (default: the target URL's origin)"
20942
+ ).option(
20943
+ "--walker <tierOrModel>",
20944
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for driving browser navigation (default: fast)"
20945
+ ).option(
20946
+ "--judge <tierOrModel>",
20947
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for human test evaluation (default: thorough)"
20948
+ ).option(
20949
+ "--matcher <tierOrModel>",
20950
+ "Model tier ('fast', 'thorough', 'inherit') or model ID for finding matching (default: thorough)"
20728
20951
  ).option(
20729
20952
  "--check",
20730
20953
  "Enforce target URL reachability (fail fast if server is offline)"
@@ -70,11 +70,9 @@ Continuity: steps form ONE ongoing monologue. From step 2 onward, don't restate
70
70
 
71
71
  ## Pacing
72
72
 
73
- **Aim for 10-20 steps.** Stop when the primary task verdict is clear — either the persona accomplished their goal or they hit friction that blocks them. A focused 12-step walkthrough with sharp findings is worth more than a 40-step exhaustive tour.
73
+ **Aim for 10-20 steps.** When evaluating a form or journey, inspect all relevant sections across the workflow (e.g., travel details, per diem, deductions, expenses). Do not abandon the walkthrough after logging a single defect — continue through the remaining sections to uncover all distinct compliance and usability gaps before completing. A complete walkthrough with sharp findings across all sections gives the team high-value signal.
74
74
 
75
- Do not try to complete every interaction. If a form has 15 fields, the observation is "this persona would give up at field 4" — note the finding and move on.
76
-
77
- Do try key interactions that reveal something: click the main CTA, open a dropdown, check what filters exist. These are cheap observations with high signal.
75
+ Do try key interactions that reveal something: click the main CTA, open a dropdown, check what options or deductions exist. These are cheap observations with high signal.
78
76
 
79
77
  Follow the persona's motivation. Spend time on what matters to them. When you have enough evidence to answer "would this person come back?" — you're done.
80
78
 
@@ -100,6 +98,20 @@ Your browser tooling has limitations that can look like product bugs. Before rec
100
98
 
101
99
  Do NOT silently suppress findings you suspect are tooling issues. Record them with the caveat — the human triaging can verify.
102
100
 
101
+ ## Expected state vs product bugs (Empty state & unseeded data)
102
+
103
+ When an environment is unseeded or test records are missing:
104
+ - An empty table or "No records found" screen is NOT automatically a product bug. If your scenario assumes existing data (e.g. reviewing past entries or auditing submitted claims), check whether the environment simply lacks seed data.
105
+ - If your persona role is a creator (e.g. employee or consultant creating an item), click the creation CTA (e.g. "+ Ny reseräkning", "+ New") and proceed with your task.
106
+ - If your persona role is read-only (e.g. an auditor or manager) and cannot create data, do NOT record false "missing data" bugs against the product. Conclude the walk noting that the environment lacks the required preconditions.
107
+
108
+ ## Semantic squinting (Adapting to environmental mismatch)
109
+
110
+ Your prompt may include an approximation strategy (e.g. "If Malmö is missing, choose Göteborg").
111
+ - **Squint at the closest match:** When the exact target name, category, or record described in your brief isn't available in this environment, pick the closest available alternative that allows testing the workflow.
112
+ - **Stay in the journey:** Approximation is bounded. Never drift to unrelated modules (e.g. settings, profile, admin panels) just because a specific test record was absent.
113
+ - **Log the approximation:** In your step note (`note`), state clearly that you chose the closest alternative according to your instructions.
114
+
103
115
  ## Portal-rendered UI
104
116
 
105
117
  Radix / Headless UI / Floating UI render dropdowns and listboxes into portals that do not appear in accessibility snapshots. If an expandable combobox looks empty after click, try: press ArrowDown then Enter, or evaluate a script in the page to probe the DOM. Take a fresh screenshot.
@@ -95,9 +95,13 @@ Human Checkpoint: <Concrete binary yes/partial/no question scored at evaluation>
95
95
  Roles with Stakes:
96
96
  • <Role 1>: <Stake>
97
97
  • <Role 2>: <Stake>
98
+
99
+ Execution Preconditions (resolved at iamu-run):
100
+ • Assumed State: <What records/data should exist, e.g. "Department with existing risk register containing open hazards">
101
+ • Squint Guidance: <How the walker should approximate if exact records are missing, e.g. "If no packaging hazard exists, select any physical safety hazard and proceed with delegation">
98
102
  ```
99
103
 
100
- Ask the user: "Ready to test this draft, or would you like to refine the focus question or circumstances? (Enter to accept, or type adjustments)"
104
+ Ask the user: "Ready to test this draft, or would you like to refine the focus question, preconditions, or circumstances? (Enter to accept, or type adjustments)"
101
105
 
102
106
  ### 4. Pick or propose personas
103
107
 
@@ -164,12 +168,12 @@ The `--role` and `--disposition` flags populate frontmatter directly. If `iamu/c
164
168
 
165
169
  **4. Scenario** (the binding):
166
170
  ```bash
167
- iamu new scenario --name "maria-gomez--busy-shift-handoff" --outline busy-shift-handoff --persona maria-gomez --json
171
+ iamu new scenario --name "maria-gomez--busy-shift-handoff" --outline busy-shift-handoff --persona maria-gomez [--entry /path/on/the/product] [--state-description "<Assumed state description>"] [--state-approximation "<Squint guidance for walker>"] --body "<First-person brief in persona voice (3–5 sentences)>" --json
168
172
  ```
169
173
  When the situation begins on a specific page (the persona follows a link from a video, an email, a search result), add `--entry /path/on/the/product` — the run starts there instead of the home page.
170
- `--outline` is required. `--persona` is optional (omit for drafts). Write the first-person brief as the markdown body — 3–5 sentences in the persona's voice.
174
+ `--outline` is required. `--persona` is optional (omit for drafts). Pass `--state-description` and `--state-approximation` if preconditions were defined in step 3 to populate the scenario's `state:` frontmatter automatically.
171
175
 
172
- For drafts (no persona): omit `--persona`, omit the brief body.
176
+ For drafts (no persona): omit `--persona`, omit `--body`.
173
177
 
174
178
  ### 7. Summarize
175
179
 
@@ -39,7 +39,6 @@ Read `iamu/context.yml`. Extract and hold:
39
39
  - **Domain + industry** — what world does this product operate in?
40
40
  - **Actors** — real roles with real responsibilities. These are scenario and persona seed material.
41
41
  - **Governed_by** — regulations and standards (if applicable). These create structural tensions in regulated domains.
42
- - **Glossary** — domain vocabulary. Scenarios and personas must use these terms, not generic UI words.
43
42
  - **Notes body** — extended context, often containing workflow descriptions, pain points, organizational dynamics.
44
43
 
45
44
  ### 2. Load existing coverage
@@ -116,7 +115,7 @@ Next action:
116
115
  Ground rules for generation:
117
116
 
118
117
  - **Start from the actor's week, not the feature list.** What does this person actually do on a Tuesday? What goes wrong? What requires judgment?
119
- - **Use domain vocabulary from the glossary.** If the glossary says "skyddsombud" not "safety representative," use "skyddsombud."
118
+ - **Use authentic domain vocabulary.** Speak in the language of the domain and the product UI (e.g., "skyddsombud" rather than generic "safety representative"), avoiding abstract test jargon.
120
119
  - **Prefer mundane frequency over dramatic edge cases.** The scenario that happens every week reveals more than the one that happens once a year.
121
120
  - **Structural tensions come from the domain, not from product limitations.** A real tension: "the regulation requires immediate reporting but the coordinator handles 3 sites." A fake tension: "the CSV upload has a 50-row limit."
122
121
  - **Never mention the product, its UI, its features, or its limitations.** The scenario exists in the domain. The product encounter happens during the walkthrough.