@aarwitz/tapp 0.16.5 → 0.17.0-rc.2

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/bin/tapp.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // tapp CLI — ship with proof.
3
3
  //
4
4
  // Zero-config verbs (the same engine the MCP tools use, exported by mcp-server/src/index.js):
5
- // tapp qa <bundleId|appId|url> Autonomous QAverdict + findings + evidence
5
+ // tapp explore <bundleId|appId|url> Autonomous exploration → findings + evidence (observation)
6
6
  // tapp open <bundleId> Launch app → screen summary + screenshot file
7
7
  // tapp tree <bundleId> Accessibility tree of the current screen
8
8
  // tapp shot Screenshot the booted simulator
@@ -30,11 +30,12 @@ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "
30
30
 
31
31
  // Redirect all writable output away from the (possibly read-only) package dir.
32
32
  // The old environment alias remains a read-only fallback for older integrations.
33
- const tappHome = (process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp")).trim();
33
+ const tappHome = (process.env.TAPP_HOME || path.join(os.homedir(), ".tapp")).trim();
34
34
  process.env.TAPP_HOME = tappHome;
35
- fs.mkdirSync(tappHome, { recursive: true });
35
+ // TAPP_HOME is created lazily (just before the switch) so `--help`, `help`, and `version` never
36
+ // write anything — not even the home directory.
36
37
 
37
- const [, , command = "help", ...rest] = process.argv;
38
+ let [, , command = "help", ...rest] = process.argv;
38
39
 
39
40
  function run(cmd, args, opts = {}) {
40
41
  const result = spawnSync(cmd, args, { encoding: "utf8", ...opts });
@@ -129,11 +130,32 @@ function repeatedFlagValues(argv, name) {
129
130
  return values;
130
131
  }
131
132
 
133
+ function iosLaunchOptions(flags, argv) {
134
+ const appLaunchArgs = repeatedFlagValues(argv, "launch-arg");
135
+ let appLaunchEnv;
136
+ if (typeof flags["launch-env"] === "string") {
137
+ try {
138
+ const parsed = JSON.parse(flags["launch-env"]);
139
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object" || Object.values(parsed).some((value) => typeof value !== "string")) {
140
+ throw new Error("expected a JSON object with string values");
141
+ }
142
+ appLaunchEnv = parsed;
143
+ } catch (error) {
144
+ console.error(`❌ --launch-env must be a JSON object with string values: ${error.message}`);
145
+ process.exit(2);
146
+ }
147
+ }
148
+ return {
149
+ ...(appLaunchArgs.length ? { appLaunchArgs } : {}),
150
+ ...(appLaunchEnv ? { appLaunchEnv } : {}),
151
+ };
152
+ }
153
+
132
154
  const engineImport = () => import(path.join(packageRoot, "mcp-server", "src", "index.js"));
133
155
 
134
156
  function requireMacFor(what) {
135
157
  if (process.platform === "darwin") return;
136
- console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp qa https://localhost:3000`);
158
+ console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp explore https://localhost:3000`);
137
159
  process.exit(1);
138
160
  }
139
161
 
@@ -196,6 +218,47 @@ async function resolveTargetOrExit(engine, input) {
196
218
  return resolved.bundleId;
197
219
  }
198
220
 
221
+ function safeCommandUsage(verb) {
222
+ const usage = {
223
+ explore: "tapp explore [target] [--platform ios|android|web] [--actions N] [--timeout SEC] [--email VALUE] [--password VALUE] [--baseline FILE] [--json FILE]\n iOS launch configuration: [--launch-arg VALUE ...] [--launch-env '{\"KEY\":\"VALUE\"}']\n Android: [--app-id ID] [--apk FILE] [--serial ID] [--keep-data]",
224
+ init: "tapp init [repo] [--explore] [--refresh] [--platform PLATFORM] [--target NAME] [--url URL] [--dry-run]",
225
+ open: "tapp open [target] [--platform ios|android|web] [--out FILE] [--tap TEXT] [--wait-for TEXT]",
226
+ tree: "tapp tree [target] [--platform ios|android|web] [--json] [--tap TEXT] [--wait-for TEXT]",
227
+ shot: "tapp shot [--out FILE]",
228
+ apps: "tapp apps",
229
+ build: "tapp build [repo] [--scheme NAME] [--configuration NAME]",
230
+ flow: "tapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
231
+ task: "tapp task validate FILE [--platform PLATFORM] [--map FILE]\ntapp task compile FILE --platform PLATFORM [--inputs JSON] [--out FILE]\ntapp task run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID] [--inputs JSON]",
232
+ contract: "tapp contract validate FILE [--platform PLATFORM] [--map FILE]\ntapp contract compile FILE --platform PLATFORM [--out FILE]\ntapp contract run FILE --platform PLATFORM [--url URL|--bundle-id ID|--app-id ID]",
233
+ scenario: "tapp scenario validate FILE [--project-dir DIR]\ntapp scenario run FILE --platform web --url URL [--project-dir DIR]",
234
+ map: "tapp map build MARKERS [--platform PLATFORM] [--out FILE] [--replace]\ntapp map inspect [FILE]\ntapp map diff BEFORE AFTER [--comparable]",
235
+ pr: "tapp pr plan [--base REF|--changed-files FILE] [--head REF] [--platform PLATFORM] [--out FILE]\ntapp pr gate PLAN [gate target/options]\ntapp pr adopt PLAN --item ID [--project-dir DIR]",
236
+ plan: "tapp plan show [FILE]\ntapp plan review [FILE] --approve NAME[,NAME] --reject NAME[,NAME] --defer NAME[,NAME]\ntapp plan generate|validate|promote [FILE] [options]",
237
+ baseline: "tapp baseline create [repo] [--platform PLATFORM] [--target NAME] [--from GATE.json] [--replace]",
238
+ actor: "tapp actor set NAME --email-env ENV --password-env ENV [--project-dir DIR]\ntapp actor list [repo]",
239
+ app: "tapp app [repo] [--no-open] [--port PORT]",
240
+ report: "tapp report [captureId|latest]",
241
+ doctor: "tapp doctor",
242
+ install: "tapp install",
243
+ mcp: "tapp mcp",
244
+ };
245
+ return usage[verb] || `tapp ${verb}`;
246
+ }
247
+
248
+ // Safe help: `--help`/`-h` on ANY verb prints the command reference and does NOTHING else — never
249
+ // builds, launches, writes, or opens (ADR-0005 manual-testing requirement). `ci` keeps its own
250
+ // richer `--help` (a safe usage print in ci-gate.sh); help/version don't need interception.
251
+ if ((rest.includes("--help") || rest.includes("-h")) && !["help", "version", "--version", "-v", "ci"].includes(command)) {
252
+ console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens. Full command reference:\n`);
253
+ command = "help";
254
+ rest = [];
255
+ }
256
+
257
+ // Create TAPP_HOME only for commands that actually use it — never for help/version/--help.
258
+ if (!["help", "version", "--version", "-v"].includes(command)) {
259
+ fs.mkdirSync(tappHome, { recursive: true });
260
+ }
261
+
199
262
  switch (command) {
200
263
  case "mcp": {
201
264
  // Agents spawn `tapp mcp`; the engine module is import-safe, so start explicitly.
@@ -256,7 +319,7 @@ switch (command) {
256
319
  testEmail: typeof flags.email === "string" ? flags.email : undefined,
257
320
  testPassword: typeof flags.password === "string" ? flags.password : undefined,
258
321
  runExploration: engine?.runInitExploration,
259
- onProgress: (progress) => process.stderr.write(`\r🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${platform === "web" ? "pages" : "screens"} reached `),
322
+ onProgress: (progress) => process.stderr.write(`\r🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached"} `),
260
323
  onStatus: (status) => console.error(`⏳ ${status}`),
261
324
  outDir,
262
325
  maxContracts,
@@ -280,7 +343,7 @@ switch (command) {
280
343
  console.log(`🧭 Tapp init — ${built.model.application.name}`);
281
344
  console.log(` targets: ${built.model.targets.length ? built.model.targets.map((target) => `${target.platform}:${target.name}`).join(", ") : "none"}`);
282
345
  console.log(` UI Map: ${built.model.uiMap.status} · ${built.model.uiMap.nodeCount} states · ${built.model.uiMap.edgeCount} transitions`);
283
- if (exploration) console.log(` Exploration: ${exploration.verdict}${exploration.inconclusive ? " (inconclusive)" : ""} · ${exploration.uiMap.nodeCount} states · evidence: ${exploration.reportHtml || exploration.capture?.path || "capture recorded"}`);
346
+ if (exploration) console.log(` Exploration: ${(exploration.findings || []).length} finding(s)${exploration.inconclusive ? " (inconclusive)" : ""} · ${exploration.uiMap.nodeCount} states · evidence: ${exploration.reportHtml || exploration.capture?.path || "capture recorded"}`);
284
347
  if (exploration?.managedRuntime) console.log(` Managed web runtime: built/started ${exploration.target} for exploration and stopped it afterward · log: ${exploration.runtime.logPath}`);
285
348
  console.log(` release plan: ${(written?.plan || built.plan).items.length} item(s) · ${pending.length} pending review · ${blocking.length} blocking requirement(s)`);
286
349
  for (const requirement of built.model.requirements) console.log(` ${requirement.severity === "blocking" ? "❌" : "⚠️"} ${requirement.message} Next: ${requirement.remediation}`);
@@ -387,9 +450,14 @@ switch (command) {
387
450
  // ---- Zero-config verbs: the same engine the MCP tools use (exported by index.js),
388
451
  // invokable by any agent or human with no server setup at all.
389
452
 
453
+ case "explore":
390
454
  case "qa": {
455
+ // `explore` is the canonical verb (ADR-0005: exploration observes; the gate judges). `qa` is a
456
+ // hidden deprecated alias.
457
+ if (command === "qa") console.error("note: 'qa' is now 'explore' — 'qa' still works for now.\n");
391
458
  const { flags, positionals } = parseVerbArgs(rest);
392
- const target = positionals[0] || "";
459
+ const launchOptions = iosLaunchOptions(flags, rest);
460
+ let target = positionals[0] || "";
393
461
  let baselineFindings;
394
462
  if (flags.baseline) {
395
463
  try {
@@ -401,21 +469,61 @@ switch (command) {
401
469
  }
402
470
  }
403
471
  const engine = await engineImport();
472
+ // Source-preparing bare explore (ADR-0005 §5): no explicit target + a repo application model →
473
+ // drive the model's default target end to end. Managed web is built/started/waited-for and
474
+ // always stopped; iOS is built + installed on the simulator; Android is built to an APK +
475
+ // installed. `--platform`/`--target` narrow which model target is chosen. With no model we fall
476
+ // through to the ordinary target resolution below, so nothing regresses.
477
+ if (!target && !flags["app-id"] && !flags.apk) {
478
+ const modelPath = existingProjectArtifactPath(process.cwd(), "application-model.json");
479
+ if (modelPath && fs.existsSync(modelPath)) {
480
+ const modelPlatform = typeof flags.platform === "string" ? flags.platform.toLowerCase() : "";
481
+ if (modelPlatform === "ios") requireMacFor("iOS testing");
482
+ const onProgress = (p) =>
483
+ process.stderr.write(`\r🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} states observed `);
484
+ const r = await engine.runExploreTarget({
485
+ projectDir: process.cwd(),
486
+ platform: modelPlatform,
487
+ target: typeof flags.target === "string" ? flags.target : "",
488
+ maxActions: flags.actions,
489
+ timeout: flags.timeout,
490
+ testEmail: flags.email,
491
+ testPassword: flags.password,
492
+ ...launchOptions,
493
+ baselineFindings,
494
+ surface: "cli",
495
+ onProgress,
496
+ onStatus: (t) => console.error(`ℹ️ ${t}`),
497
+ });
498
+ process.stderr.write("\n");
499
+ if (r.error) { printEngineError(r); process.exit(1); }
500
+ console.log(r.text);
501
+ if (flags.json && typeof flags.json === "string") {
502
+ fs.writeFileSync(flags.json, JSON.stringify(r.structured, null, 2));
503
+ console.log(`\n📄 Full report JSON: ${flags.json} (pass as --baseline next run to diff regressions)`);
504
+ }
505
+ break;
506
+ }
507
+ }
404
508
  const platform = requestedPlatform(flags, target);
405
509
  if (!["ios", "android", "web"].includes(platform)) {
406
510
  console.error("❌ --platform must be ios|android|web");
407
511
  process.exit(2);
408
512
  }
409
513
  if (platform === "ios") requireMacFor("iOS testing");
514
+ if (platform !== "ios" && Object.keys(launchOptions).length) {
515
+ console.error("❌ --launch-arg and --launch-env apply only to iOS targets");
516
+ process.exit(2);
517
+ }
410
518
  if (platform === "web" && !/^https?:\/\//i.test(target)) {
411
519
  console.error("❌ Web QA needs an http(s) URL");
412
520
  process.exit(2);
413
521
  }
414
522
  const bundleId = platform === "ios" ? await resolveTargetOrExit(engine, target) : null;
415
523
  const android = platform === "android" ? androidTarget(flags, target) : null;
416
- const unit = platform === "web" ? "pages" : "screens";
524
+ const progressMetric = platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached";
417
525
  const onProgress = (p) =>
418
- process.stderr.write(`\r🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${unit} reached `);
526
+ process.stderr.write(`\r🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${progressMetric} `);
419
527
  const r = platform === "web"
420
528
  ? await engine.runQaWeb({
421
529
  url: target,
@@ -443,7 +551,7 @@ switch (command) {
443
551
  bundleId,
444
552
  maxActions: flags.actions,
445
553
  timeout: flags.timeout,
446
- args: { testEmail: flags.email, testPassword: flags.password, baselineFindings },
554
+ args: { testEmail: flags.email, testPassword: flags.password, baselineFindings, ...launchOptions },
447
555
  surface: "cli",
448
556
  onProgress,
449
557
  });
@@ -501,7 +609,6 @@ switch (command) {
501
609
  if (target.apkPath) await driver.install(target.apkPath);
502
610
  const snap = await driver.launch({ clearData: flags["clear-data"] === true });
503
611
  const data = await driver.screenshot();
504
- await driver.forceStop();
505
612
  console.log(`🚀 Launched \`${target.appId}\` (Android)\n`);
506
613
  console.log(engine.formatScreen(snap.screenTitle, snap.elements));
507
614
  const out = typeof flags.out === "string" ? flags.out : path.join(tappHome, "shots", `${target.appId}-${Date.now()}.png`);
@@ -561,12 +668,17 @@ switch (command) {
561
668
  break;
562
669
  }
563
670
  if (platform === "android") {
564
- const target = androidTarget(flags, positionals[0] || "");
671
+ const input = positionals[0] || "";
672
+ const hasTarget = !!(input || flags["app-id"] || flags.apk);
673
+ const target = hasTarget
674
+ ? androidTarget(flags, input)
675
+ : { serial: typeof flags.serial === "string" ? flags.serial : undefined };
565
676
  const { AndroidDriver } = await import(path.join(packageRoot, "mcp-server", "src", "android-driver.js"));
566
677
  const driver = new AndroidDriver(target);
567
678
  await driver.ensureDevice();
568
- const snap = await driver.snapshot();
569
- if (flags.json) console.log(JSON.stringify({ screenTitle: snap.screenTitle, elements: snap.elements }, null, 2));
679
+ if (target.apkPath) await driver.install(target.apkPath);
680
+ const snap = target.appId ? await driver.launch() : await driver.snapshot();
681
+ if (flags.json) console.log(JSON.stringify({ platform: "android", appId: target.appId || null, activity: snap.activity, screenTitle: snap.screenTitle, elements: snap.elements }, null, 2));
570
682
  else console.log(engine.formatScreen(snap.screenTitle, snap.elements));
571
683
  break;
572
684
  }
@@ -625,7 +737,7 @@ switch (command) {
625
737
  }
626
738
  console.log("📱 Installed on the booted simulator:\n");
627
739
  for (const a of la.apps) console.log(` ${a.bundleId} (${a.name})`);
628
- console.log(`\nTest one: tapp qa <bundleId>`);
740
+ console.log(`\nTest one: tapp explore <bundleId>`);
629
741
  break;
630
742
  }
631
743
 
@@ -655,7 +767,7 @@ switch (command) {
655
767
  process.exit(1);
656
768
  }
657
769
  console.log(`🔨 Built ${path.basename(built.appPath)} (scheme ${built.scheme}) — installed as ${inst.bundleId}`);
658
- console.log(`\nNext: tapp qa ${inst.bundleId}`);
770
+ console.log(`\nNext: tapp explore ${inst.bundleId}`);
659
771
  break;
660
772
  }
661
773
 
@@ -1065,7 +1177,7 @@ switch (command) {
1065
1177
 
1066
1178
  console.log(`\n Home: ${tappHome}`);
1067
1179
  console.log(healthy
1068
- ? "\nReady. Start with:\n npx -y @aarwitz/tapp open [target]\n npx -y @aarwitz/tapp qa [target]"
1180
+ ? "\nReady. Start with:\n npx -y @aarwitz/tapp open [target]\n npx -y @aarwitz/tapp explore [target]"
1069
1181
  : "\nFix the ❌ items above, then re-run: tapp doctor");
1070
1182
  process.exit(healthy ? 0 : 1);
1071
1183
  }
@@ -1249,7 +1361,7 @@ switch (command) {
1249
1361
  replace: flags.replace === true,
1250
1362
  });
1251
1363
  console.log(`✅ Conclusive baseline established — ${selectedTarget.platform}:${selectedTarget.name}`);
1252
- console.log(` ${written.validation.screensExplored} states · ${written.validation.actionsPerformed} actions · ${written.validation.suite.contracts} contracts · verdict ${written.validation.verdict}`);
1364
+ console.log(` ${written.validation.screensExplored} states · ${written.validation.actionsPerformed} actions · ${written.validation.suite.contracts} contracts · outcome ${written.validation.outcome}`);
1253
1365
  console.log(` baseline: ${written.path}\n source gate report: ${reportPath}`);
1254
1366
  } catch (error) { console.error(`❌ Baseline not written: ${error.message}`); process.exit(2); }
1255
1367
  break;
@@ -1372,15 +1484,32 @@ switch (command) {
1372
1484
  }
1373
1485
 
1374
1486
  default: {
1375
- console.log(`tapp v${pkg.version} — ship with proof. Autonomous QA and deterministic Flows for iOS, Android, and web.
1487
+ console.log(`tapp v${pkg.version} — ship with proof. Autonomous exploration and deterministic release gates for iOS, Android, and web.
1376
1488
 
1377
- Zero-config verbs (agents and humans can just run these — no server, no setup):
1489
+ Core explore, prove, gate (agents and humans can just run these — no server, no setup):
1490
+ tapp explore [target] Autonomous exploration → findings + evidence (an observation, NOT a
1491
+ release decision — run 'tapp ci' to gate a merge)
1492
+ (--platform ios|android|web · --app-id ID · --apk FILE · --actions N)
1493
+ tapp contract run FILE Replay a business-level release contract — the guarantees that must hold
1494
+ tapp ci ... Merge-blocking release gate — explore + suites + baseline → pass/fail/inconclusive
1495
+ (see: tapp ci --help)
1496
+
1497
+ Primitives — an agent's eyes and hands (no setup):
1378
1498
  tapp open [target] Launch the app → screen summary + screenshot saved to a file
1379
1499
  (web: --tap TEXT · --wait-for TEXT · --out FILE)
1380
- tapp qa [target] Autonomous QA → verdict + findings + evidence
1381
- (--platform ios|android|web · --app-id ID · --apk FILE · --actions N)
1382
1500
  tapp tree [target] Accessibility tree of the current screen (--json for every element)
1383
1501
  (web: --tap TEXT · --wait-for TEXT)
1502
+
1503
+ Repository & release:
1504
+ tapp init [repo] Detect targets and write the application model + reviewable release plan
1505
+ (--explore grounds the UI Map · --url URL · --platform · --dry-run · --refresh)
1506
+ tapp baseline create [repo] Run/import a conclusive full gate and save a target-scoped baseline
1507
+ tapp report [captureId] Open the HTML evidence page for a capture (default: latest)
1508
+ tapp ci install [repo] Generate a reviewable target-aware GitHub workflow + CI manifest
1509
+ tapp actor set NAME Configure an actor using environment-variable names only (never values)
1510
+ tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
1511
+
1512
+ Advanced — deterministic suites, lifecycle & compilers:
1384
1513
  tapp flow run FILE Replay a committed deterministic Flow (no AI/API key)
1385
1514
  tapp flow validate FILE Validate a Flow without launching a target
1386
1515
  tapp task validate FILE Validate a reusable deterministic Task (+ optional UI Map grounding)
@@ -1388,38 +1517,32 @@ Zero-config verbs (agents and humans can just run these — no server, no setup)
1388
1517
  tapp task run FILE Replay a Task directly on iOS, Android, or web
1389
1518
  tapp contract validate FILE Validate a business-level TypeScript release contract
1390
1519
  tapp contract compile FILE Compile a contract to the shared deterministic executor
1391
- tapp contract run FILE Replay a release contract without AI or a coding agent
1392
- tapp pr plan --base REF Select critical + diff-relevant contracts and report uncovered changes
1393
- tapp pr adopt PLAN --item ID Explicitly add an observed PR coverage proposal to the release plan
1394
1520
  tapp scenario run FILE Replay an isolated multi-actor system Scenario (web)
1395
1521
  tapp scenario validate FILE Validate actors, lifecycle, and deterministic steps
1522
+ tapp pr plan --base REF Select critical + diff-relevant contracts and report uncovered changes
1523
+ tapp pr adopt PLAN --item ID Explicitly add an observed PR coverage proposal to the release plan
1524
+ tapp plan show [FILE] Inspect the proposed/accepted release-contract plan
1525
+ tapp plan review [FILE] Explicitly approve, reject, or defer proposed plan items
1526
+ tapp plan generate [FILE] Generate compile-checked, untrusted contract drafts from approved Tasks
1527
+ tapp plan validate [FILE] Replay drafts on a real target; trust only after all platforms pass
1528
+ tapp plan promote [FILE] Move fully validated drafts into reviewed Tasks/contracts + map coverage
1396
1529
  tapp map build MARKERS Build/merge the persistent platform-neutral UI Map
1397
1530
  tapp map inspect [FILE] Inspect states, controls, platforms, and map validity
1398
1531
  tapp map diff A B Diff observed UI structure without false reachability claims
1399
- tapp baseline create [repo] Run/import a conclusive full gate and save a target-scoped baseline
1532
+
1533
+ Simulator & workspace:
1400
1534
  tapp shot Screenshot the booted simulator → file path (--out file.jpg)
1401
1535
  tapp build [dir] Build the iOS app in a repo for the simulator + install it (--scheme S)
1402
1536
  tapp apps List apps installed on the booted simulator (with bundle ids)
1403
- tapp report [captureId] Open the HTML evidence page for a capture (default: latest)
1404
1537
  tapp app [repo] Optional local browser workspace for repository onboarding and review
1405
1538
  (loopback-only; --no-open · --port PORT)
1406
- tapp init [repo] Detect targets and write the application model + reviewable release plan
1407
- (--explore builds/starts or connects, grounds the UI Map, then tears down)
1408
- (--url URL · --platform PLATFORM · --dry-run · --refresh)
1409
- tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
1410
- tapp actor set NAME Configure an actor using environment-variable names only (never values)
1411
- tapp plan show [FILE] Inspect the proposed/accepted release-contract plan
1412
- tapp plan review [FILE] Explicitly approve, reject, or defer proposed plan items
1413
- tapp plan generate [FILE] Generate compile-checked, untrusted contract drafts from approved Tasks
1414
- tapp plan validate [FILE] Replay drafts on a real target; trust only after all platforms pass
1415
- tapp plan promote [FILE] Move fully validated drafts into reviewed Tasks/contracts + map coverage
1416
- tapp ci ... Merge-blocking release gate — explore + flows + baseline diff (see: tapp ci --help)
1417
- tapp ci install [repo] Generate a reviewable target-aware GitHub workflow + CI manifest
1418
1539
 
1419
- [target] is whatever you have — nothing (finds + builds the Xcode project in the current
1420
- dir, or falls back to the app on the simulator), a repo dir, a path/to/App.app, a bundle
1421
- id, an Android app id/APK (--platform android --app-id ...), or an http(s) URL.
1422
- For iOS you never need to know a bundle id up front.
1540
+ [target] is whatever you have — nothing (in an initialized repo, bare 'tapp explore' drives the
1541
+ application model's default target from source: managed web is built/started/stopped, iOS is
1542
+ built + installed, Android is built to an APK + installed; otherwise it finds + builds the Xcode
1543
+ project in the current dir, or falls back to the app on the simulator), a repo dir, a
1544
+ path/to/App.app, a bundle id, an Android app id/APK (--platform android --app-id ...), or an
1545
+ http(s) URL. For iOS you never need to know a bundle id up front.
1423
1546
 
1424
1547
  Setup:
1425
1548
  tapp install Prebuild the exploration harness (~2 min; otherwise builds on first use)
@@ -1432,7 +1555,7 @@ MCP hookup (optional — for inline screenshots and the tap/type/inspect session
1432
1555
  { "servers": { "tapp": { "type": "stdio", "command": "npx", "args": ["-y", "@aarwitz/tapp", "mcp"] } } }
1433
1556
 
1434
1557
  Then ask your agent things like:
1435
- "Run tapp qa on com.mycompany.app is it ship-ready?"
1558
+ "Explore com.mycompany.app and show me what breaks"
1436
1559
  "Open the settings screen and show me the screenshot"
1437
1560
  "Drive the login flow and record it as a replayable test"
1438
1561
 
package/browser/app.js CHANGED
@@ -243,12 +243,14 @@ function renderDecision(run) {
243
243
  return;
244
244
  }
245
245
  const report = latest.report;
246
- const failed = report.gate?.failed === true;
247
- const verdict = failed ? "blocked" : report.inconclusive ? "caution" : report.verdict || "caution";
248
- card.className = `decision-card ${verdict}`;
249
- $("#decision-title").textContent = failed ? "Do not merge" : report.inconclusive ? "Inconclusive" : report.verdict === "ready" ? "Ready to merge" : "Review required";
246
+ // The decision reflects the GATE outcome (pass/fail/inconclusive), not a ship verdict — exploration
247
+ // only observes (ADR-0005). Without a gate, it's an observation, not a merge decision.
248
+ const outcome = report.gate?.outcome || (report.gate?.failed === true ? "fail" : report.inconclusive ? "inconclusive" : report.gate ? "pass" : null);
249
+ const cssClass = { pass: "ready", fail: "blocked", inconclusive: "caution" }[outcome] || "caution";
250
+ card.className = `decision-card ${cssClass}`;
251
+ $("#decision-title").textContent = outcome === "fail" ? "Do not merge" : outcome === "inconclusive" ? "Inconclusive" : outcome === "pass" ? "Ready to merge" : "Observed — not a release decision";
250
252
  $("#decision-detail").textContent = report.gate?.reasons?.join(" · ") || report.headline || "Review the evidence below.";
251
- $("#overview-evidence").innerHTML = `<div class="latest-run-line"><span class="verdict-dot ${esc(verdict)}"></span><div><strong>${esc(report.headline || pretty(verdict))}</strong><small>${esc(pretty(report.platform || "unknown"))} · ${compactDate(latest.createdAt)} · ${(report.contracts || []).filter((item) => item.passed).length}/${(report.contracts || []).length} contracts passed</small></div></div><p>${esc((report.gate?.reasons || ["No blocking release-gate reason reported."])[0])}</p>${reportLink(report)}`;
253
+ $("#overview-evidence").innerHTML = `<div class="latest-run-line"><span class="verdict-dot ${esc(cssClass)}"></span><div><strong>${esc(report.headline || pretty(cssClass))}</strong><small>${esc(pretty(report.platform || "unknown"))} · ${compactDate(latest.createdAt)} · ${(report.contracts || []).filter((item) => item.passed).length}/${(report.contracts || []).length} contracts passed</small></div></div><p>${esc((report.gate?.reasons || ["No blocking release-gate reason reported."])[0])}</p>${reportLink(report)}`;
252
254
  }
253
255
 
254
256
  function renderEvidence(runs) {
@@ -284,7 +286,11 @@ function renderRuns(runs) {
284
286
  $("#runs-list").innerHTML = runs.length ? runs.map((run) => {
285
287
  const report = run.report;
286
288
  const failed = report?.gate?.failed === true;
287
- const status = !report ? run.status : failed ? "blocked" : report.inconclusive ? "inconclusive" : report.verdict || "completed";
289
+ // Status reflects the gate outcome (pass/fail/inconclusive), mapped to the existing CSS classes.
290
+ const status = !report ? run.status
291
+ : report.gate?.outcome === "fail" || failed ? "blocked"
292
+ : report.gate?.outcome === "inconclusive" || report.inconclusive ? "inconclusive"
293
+ : report.gate?.outcome === "pass" ? "ready" : "completed";
288
294
  return `<button class="run-row ${run.id === state.selectedRunId ? "selected" : ""}" data-run-id="${esc(run.id)}"><span class="run-status ${esc(status)}">${failed ? "×" : report ? "✓" : "…"}</span><span><strong>${esc(report?.headline || `Release run ${run.id.slice(-8)}`)}</strong><small>${compactDate(run.createdAt)} · ${esc(pretty(report?.platform || "unknown"))}</small></span><em>${esc(pretty(status))}</em></button>`;
289
295
  }).join("") : '<div class="empty">No release runs yet.</div>';
290
296
  const selected = runs.find((run) => run.id === state.selectedRunId) || runs[0];
@@ -0,0 +1,75 @@
1
+ # Browser Release Studio
2
+
3
+ Status: current local-product contract as of 2026-08-08.
4
+
5
+ The browser Release Studio is an **optional local workspace**, not the current launch surface — the
6
+ **npm package (CLI + MCP + the GitHub Action) is the current objective** (ADR-0005). The Studio, the
7
+ VS Code extension, the desktop app, and the future managed SaaS are separate/paused tracks. Web is
8
+ also one application target beside iOS and Android; it is not a separate QA product. CLI, MCP, VS
9
+ Code, the Action, desktop, and future managed SaaS all adapt the shared product operations described
10
+ in [`PRODUCT-ENGINE.md`](PRODUCT-ENGINE.md).
11
+
12
+ ## Start locally
13
+
14
+ ```bash
15
+ npx -y @aarwitz/tapp app
16
+ ```
17
+
18
+ Tapp prints an authenticated one-time launch URL and opens it in the default browser. Use
19
+ `--no-open` when copying the URL manually and `--port 4317` only when a fixed loopback port is
20
+ needed. Drag/drop or Browse Folder copies source into a Tapp-owned workspace. Connect GitHub lists
21
+ repositories authorized to the local `gh` session and makes a shallow isolated clone.
22
+ `tapp app /path/to/repo` intentionally works directly in that checkout.
23
+
24
+ The local server binds to `127.0.0.1`. It owns workspace paths; browser requests cannot submit an
25
+ arbitrary server path. Mutations require an `HttpOnly` same-site session cookie, the exact local
26
+ Origin, and an in-memory CSRF token. Application runtimes, repository credentials, and evidence stay
27
+ in the local process/filesystem. This is a local trust boundary, not hosted multi-tenancy.
28
+
29
+ ## Product journey
30
+
31
+ 1. **Connect** a copied folder, an explicit checkout, or a repository authorized by local `gh`.
32
+ 2. **Detect and choose** an iOS, Android, or web target. Continue automatically only when the target
33
+ and configuration are conclusive.
34
+ 3. **Build, launch, and explore** the real simulator, emulator/device, or browser surface.
35
+ 4. **Understand the UI Map** through observed states, transitions, controls, provenance, and gaps.
36
+ 5. **Review intent** by approving, rejecting, deferring, or constraining a compact release plan.
37
+ 6. **Generate drafts** of Tasks and contracts. Drafts remain visibly untrusted.
38
+ 7. **Validate** approved drafts deterministically against the real target.
39
+ 8. **Promote** only validated artifacts into the canonical suite and refreshed Application Model.
40
+ 9. **Gate** with autonomous evidence plus the promoted deterministic suite.
41
+ 10. **Baseline** only a passing, conclusive, target-scoped gate.
42
+ 11. **Install CI** by previewing and writing a reviewable repository patch. Tapp does not commit,
43
+ push, create GitHub secrets, or enable branch protection.
44
+
45
+ Successful semantic actions can be saved in `.tapp/flows/`; credential values are templated to
46
+ environment references. Long-lived repository artifacts store binding names, not resolved secret
47
+ values.
48
+
49
+ ## Verified reference journey
50
+
51
+ `tests/browser-journey.test.js` drives the visible local browser against a fresh CommerceDemo copy.
52
+ It exercises startup, a real live web surface and semantic action, UI Map creation, Flow recording
53
+ and replay, proposal review, generation, deterministic validation, promotion, a first gate,
54
+ baseline-aware rerun, and CI preview.
55
+
56
+ The opt-in `tests/browser-native-journey.test.js` passed on 2026-08-06 against a booted iOS
57
+ simulator: the browser built and installed a disposable DemoApp checkout, ran shared target
58
+ preparation and exploration, rendered an observed UI Map, drove the live surface, and saved a
59
+ repository-native iOS Flow. The equivalent Android browser journey was not verified in that audit
60
+ because no emulator/device was connected.
61
+
62
+ This evidence proves representative local journeys. It does not prove arbitrary frameworks,
63
+ production credentials, third-party services, hosted execution, or complete inference of business
64
+ intent.
65
+
66
+ ## Hosted relationship
67
+
68
+ The future hosted application will present the same product journey through a different adapter: application
69
+ accounts/organizations, GitHub App repository authorization, private storage, a durable queue, and
70
+ isolated managed workers. It cannot reuse the loopback session, local `gh` authority, filesystem
71
+ boundary, or in-memory ownership assumptions.
72
+
73
+ The old hosted preview and `cloud/` prototype do not satisfy this boundary. The managed SaaS is a
74
+ separate, paused track (see the private source repository); do not market or accept private
75
+ repositories until its readiness gate passes.
@@ -0,0 +1,107 @@
1
+ # One Tapp product engine
2
+
3
+ Status: current product-engine contract as of 2026-08-08.
4
+
5
+ Tapp has several interfaces, not several products. The source of truth for customer-critical
6
+ operations is [`mcp-server/src/product-operations.js`](../mcp-server/src/product-operations.js).
7
+ An interface may validate its transport and render a result; it must not redefine onboarding,
8
+ review, trust, baseline, or gate semantics.
9
+
10
+ ## Product operation contract
11
+
12
+ The shared engine owns these operations:
13
+
14
+ | Operation | Authoritative result |
15
+ |---|---|
16
+ | `initializeProductProject` | detected targets, real exploration, Application Model, UI Map, release plan |
17
+ | `readProductProject` | one current, read-only product snapshot for any interface |
18
+ | `reviewProductPlan` | explicit approve/reject/defer decisions |
19
+ | `generateProductPlan` | compile-checked but untrusted Task/contract drafts |
20
+ | `validateProductPlan` | real-target, deterministic replay evidence |
21
+ | `promoteProductPlan` | canonical Tasks/contracts, refreshed model/plan, updated map coverage |
22
+ | `prepareProductCi` / `installProductCi` | target-aware workflow and machine-readable CI manifest |
23
+ | `runProductGate` | autonomous evidence plus the committed deterministic suite and one gate decision |
24
+ | `createProductBaseline` | conclusive, platform-and-target-specific comparison state |
25
+
26
+ Deterministic contract execution is in
27
+ [`mcp-server/src/product-execution.js`](../mcp-server/src/product-execution.js). It invokes platform
28
+ executors directly; MCP does not shell through the CLI, and the browser does not shell through MCP.
29
+
30
+ ## Interfaces
31
+
32
+ ```text
33
+ Browser Release Studio ─┐
34
+ CLI ├── product-operations ── application model / UI Map / Tasks / contracts
35
+ MCP ┘ │
36
+ └── deterministic executors / portable gate / evidence
37
+
38
+ VS Code ── MCP client
39
+ Desktop ── canonical artifact reader (migration to operation client remains)
40
+ Action ── portable gate adapter
41
+ Hosted ── tenant-aware SaaS adapter + queued isolated shared-operation workers (not built)
42
+ ```
43
+
44
+ Current convergence:
45
+
46
+ - the browser calls only shared product operations;
47
+ - CLI initialization, plan lifecycle, deterministic draft validation, promotion, gate/baseline
48
+ lifecycle, and CI installation call the same operations. Native build preparation remains at the
49
+ adapter boundary and passes a resolved `.app` or APK into the shared gate;
50
+ - MCP initialization, plan lifecycle, deterministic draft validation, promotion, baseline, and CI
51
+ installation call the same operations;
52
+ - the GitHub Action and `runProductGate` call the same portable gate and evidence protocol;
53
+ - VS Code remains a thin MCP client;
54
+ - desktop reads the same `.tapp` artifacts but still has legacy import/build orchestration. It is
55
+ retained, not the launch UX, until that orchestration is removed;
56
+ - `cloud/runner` is retained prototype evidence for exact checkout, versioned operation envelopes,
57
+ leases, and cleanup. It is not the production hosted adapter or an adequate arbitrary-customer
58
+ isolation boundary. The new SaaS must call these shared operations only through a tenant-aware,
59
+ queued worker contract (defined in the private source repository's SaaS architecture doc).
60
+
61
+ ## Canonical repository protocol
62
+
63
+ New product behavior writes only `.tapp/`:
64
+
65
+ ```text
66
+ .tapp/
67
+ project.json # actors, env binding names, controlled lifecycle; never secret values
68
+ application-model.json # detected/observed/declared product facts
69
+ ui-map.json # grounded screen/action/transition graph
70
+ release-plan.json # proposals and explicit human decisions
71
+ tasks/ # reusable deterministic semantic operations
72
+ contracts/ # reviewed business guarantees
73
+ baselines/<platform>/ # conclusive target-specific comparison state
74
+ ci.json # generated CI installation manifest
75
+ ```
76
+
77
+ `.tapp.yml` is the canonical run configuration, and `.tapp/` holds repository artifacts. These are
78
+ the only names the runtime reads; the pre-rename `.autotap.yml`, `.autotap/`, and `AUTOTAP_*` inputs
79
+ are no longer supported. Do not add another configuration format, and do not reintroduce a legacy
80
+ reader. Only an explicit reviewed operation may write new repository artifacts.
81
+
82
+ ## Anti-duplication rules
83
+
84
+ 0. Exploration (`explore`, formerly `qa`) observes and surfaces findings + evidence + UI Map; it must
85
+ not render a release outcome. Only the gate (`runProductGate`) applies versioned deterministic
86
+ policy to findings + coverage + selected suites + an optional baseline and computes
87
+ `pass | fail | inconclusive` (ADR-0005, in the private source repository).
88
+ 1. Trust states (`pending`, `approved`, `validated-draft`, `promoted`) are computed by the engine.
89
+ 2. Interfaces render `readProductProject`; they do not infer readiness from file existence.
90
+ 3. Re-exploration refreshes evidence while preserving reviewed decisions everywhere.
91
+ 4. Promotion refreshes the Application Model immediately; no interface may show stale pre-promotion
92
+ requirements.
93
+ 5. Baselines are identified by platform and stable target id everywhere.
94
+ 6. An adapter-specific feature is not complete until its engine operation is useful without that
95
+ adapter.
96
+ 7. Equivalence tests should assert artifacts and structured results, not merely matching copy.
97
+
98
+ ## Remaining migration
99
+
100
+ The next safe convergence work is deliberately narrow:
101
+
102
+ 1. replace desktop import/build orchestration with a local product-operation client;
103
+ 2. delete the two desktop detection/scaffolding paths only after equivalence fixtures pass;
104
+ 3. implement managed account, organization, and tenant authorization before connecting repositories;
105
+ 4. implement scoped GitHub authorization, private evidence, and disposable per-job
106
+ identity/simulator/credential isolation before accepting customer code;
107
+ 5. preserve CLI/MCP/VS Code/Action as adapters—do not rebuild their product logic.