@aarwitz/tapp 0.17.0-rc.13 → 0.17.0-rc.15

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tapp",
3
- "description": "The official Tapp plugin: one agent skill and MCP tools for testing iOS, Android, and web apps on real surfaces.",
3
+ "description": "The official Tapp plugin: the agent skill and MCP tools for testing real iOS, Android, and web app surfaces.",
4
4
  "owner": {
5
5
  "name": "Aaron Horowitz",
6
6
  "url": "https://github.com/aarwitz"
@@ -12,12 +12,12 @@
12
12
  "source": "npm",
13
13
  "package": "@aarwitz/tapp"
14
14
  },
15
- "description": "Test real iOS, Android, and web apps with Tapp. Includes the Tapp agent skill plus MCP hands, eyes, exploration, replayable Flows, and deterministic release evidence.",
15
+ "description": "Give Claude hands and eyes on iOS, Android, and web apps, with exploration, replayable flows, evidence, and deterministic CI gates.",
16
16
  "author": {
17
17
  "name": "Aaron Horowitz",
18
18
  "url": "https://github.com/aarwitz"
19
19
  },
20
- "homepage": "https://github.com/aarwitz/tapp",
20
+ "homepage": "https://runtapp.com/",
21
21
  "license": "MIT",
22
22
  "keywords": [
23
23
  "ios",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tapp",
3
- "description": "Test real iOS, Android, and web apps with Tapp. Includes the Tapp agent skill plus MCP hands, eyes, exploration, replayable Flows, and deterministic release evidence.",
4
- "version": "0.17.0-rc.13",
3
+ "description": "Give Claude hands and eyes on iOS, Android, and web apps, with exploration, replayable flows, evidence, and deterministic CI gates.",
4
+ "version": "0.17.0-rc.15",
5
5
  "author": {
6
6
  "name": "Aaron Horowitz",
7
7
  "url": "https://github.com/aarwitz"
8
8
  },
9
- "homepage": "https://github.com/aarwitz/tapp",
9
+ "homepage": "https://runtapp.com/",
10
10
  "repository": "https://github.com/aarwitz/tapp",
11
11
  "license": "MIT",
12
12
  "keywords": [
@@ -24,7 +24,7 @@
24
24
  "command": "npx",
25
25
  "args": [
26
26
  "-y",
27
- "@aarwitz/tapp@0.17.0-rc.13",
27
+ "@aarwitz/tapp@0.17.0-rc.15",
28
28
  "mcp"
29
29
  ],
30
30
  "cwd": "${CLAUDE_PROJECT_DIR}"
package/AGENTS.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # Tapp agent playbook
2
2
 
3
- You (the agent) have Tapp: hands, eyes, and judgment on real app surfaces — iOS simulators,
4
- Android emulators/devices, plus (beta) web apps in a real browser.
3
+ You (the agent) have Tapp: hands and eyes on real app surfaces — iOS simulators, Android
4
+ emulators/devices, plus (beta) web apps in a real browser. Release judgment belongs only to Tapp's
5
+ repository-connected deterministic gate.
5
6
 
6
7
  ## No MCP connected? Just run the CLI
7
8
 
8
- Every core capability works as a plain command — no server, no config. `[target]` is
9
+ The core inspect, explore, replay, and gate capabilities work as plain commands — no Tapp account,
10
+ server, or global install. `[target]` is
9
11
  optional: with nothing, tapp finds + builds the Xcode project in the cwd (or falls back to
10
12
  the app already on the simulator); it also accepts a repo dir, a `path/to/App.app`, a
11
13
  bundle id, or (web) an http(s) URL. You never need to know a bundle id up front.
@@ -57,7 +59,7 @@ installs, returns the bundle id) → `tapp_explore {appBundleId}`.
57
59
 
58
60
  | The user wants… | Use | NOT |
59
61
  |---|---|---|
60
- | "Show me / screenshot a screen" | `tapp_open_app` (launch + screenshot + tree, ~15s) | `tapp_explore` (a full multi-minute QA exploration) |
62
+ | "Show me / screenshot a screen" | `tapp_open_app` (launch + screenshot + tree, ~15s) | `tapp_explore` (a full multi-minute exploration) |
61
63
  | "Tap through / drive / fill a form / log in" | `tapp_session_start` → `session_act` loop | repeated `open_app` calls (cold relaunch each time) |
62
64
  | "Is my app broken? Find bugs" | `tapp_explore` — `appBundleId` for iOS, `androidAppId` for Android, `url` for owned web apps; returns an observation (findings + evidence), not a ship verdict — gate a merge with the CI gate (`tapp ci` CLI / the GitHub Action) + a contract | a manual session (exploration is autonomous) |
63
65
  | "Make this flow a repeatable test" | drive it in a session, then `tapp_flow_save`; replay with `tapp_flow_run` | re-driving it by hand every time |
@@ -67,6 +69,7 @@ installs, returns the bundle id) → `tapp_explore {appBundleId}`.
67
69
 
68
70
  ```
69
71
  tapp_session_start { appBundleId: "com.acme.app" } → fresh launch + initial tree
72
+ tapp_session_act { action: "login", email: "qa@x.com", password: "…" } → atomic fill + submit + verify
70
73
  tapp_session_act { action: "tap", id: "Email" } → tap by a11y id OR visible label
71
74
  tapp_session_act { action: "type", text: "qa@x.com" } → types into the focused field
72
75
  tapp_session_act { action: "tap", id: "Sign In" }
@@ -90,7 +93,8 @@ Rules that prevent 90% of failures:
90
93
  3. **`wait` after anything async** (navigation, network loads): `{action: "wait", id|text, timeoutMs}`.
91
94
  Never assume the next screen is instantly there.
92
95
  4. **Tap the field before typing** — `type` goes to the focused field. Tap email → type email →
93
- tap password → type password.
96
+ tap password → type password. For sign-in, prefer the atomic `login` action: it records a
97
+ secret-templated replay step and avoids native secure-field refocus behavior.
94
98
  5. Tap results: `ok` (landed), `not_hittable` (exists but disabled/covered — the harness
95
99
  auto-dismisses keyboards and retries), `not_found` (nothing matches — re-read the tree).
96
100
  6. One session at a time. `session_start` always begins from a fresh app launch.
@@ -134,6 +138,8 @@ without a coding agent, model, subscription, or API key. AI generation and `asse
134
138
  deterministic assertions, same result every time. A failed assertion is a finding.
135
139
  - **Generate:** `tapp_flow_generate { goal: "log in and add the first item to cart" }` —
136
140
  grounded in the app's actually-explored screens, so it can't invent steps.
141
+ - **Discover the file format without MCP:** `npx -y @aarwitz/tapp@latest flow example` prints a
142
+ complete starter Flow; `tapp flow validate <file>` checks it without launching a target.
137
143
 
138
144
  ## Setup facts (tell the user when relevant)
139
145
 
@@ -765,6 +765,12 @@ class ExplorerTests: XCTestCase {
765
765
  case "type":
766
766
  status = sessionType(value, id: target.isEmpty ? nil : target) ? "pass" : "fail"
767
767
  if status == "fail" { detail = "no field ‘\(target)’ to type into" }
768
+ case "login":
769
+ let email = subst((step["email"] as? String) ?? "$TEST_EMAIL")
770
+ let password = subst((step["password"] as? String) ?? "$TEST_PASSWORD")
771
+ let result = sessionLogin(email: email, password: password)
772
+ status = result.status == "ok" ? "pass" : "fail"
773
+ if status == "fail" { detail = result.detail.isEmpty ? result.status : result.detail }
768
774
  case "swipe":
769
775
  switch target.lowercased() { case "down": app.swipeDown(); case "left": app.swipeLeft(); case "right": app.swipeRight(); default: app.swipeUp() }
770
776
  case "back":
@@ -1029,6 +1035,11 @@ class ExplorerTests: XCTestCase {
1029
1035
 
1030
1036
  let testEmail = resolve("OCQA_TEST_EMAIL")
1031
1037
  let testPassword = resolve("OCQA_TEST_PASSWORD")
1038
+ if resolve("OCQA_CREDENTIALS_EXPLICIT") == "1" {
1039
+ // Presence only: never print, persist, or expose credential values. Report rebuilding
1040
+ // needs this durable marker to distinguish "not supplied" from "supplied but unused".
1041
+ print("OCQA_STATE:credentials_supplied")
1042
+ }
1032
1043
 
1033
1044
  // --- Explicit login replay (config-driven): a recorded type/tap/wait sequence for custom
1034
1045
  // login UIs the heuristic preamble below can't parse. When configured it takes precedence. ---
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # tapp — ship with proof
1
+ # Tapp
2
2
 
3
3
  [![CI](https://github.com/aarwitz/tapp/actions/workflows/ci.yml/badge.svg)](https://github.com/aarwitz/tapp/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/%40aarwitz%2Ftapp?color=cb3837&label=npm)](https://www.npmjs.com/package/@aarwitz/tapp)
@@ -9,29 +9,24 @@
9
9
  [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_MCP-000000)](cursor://anysphere.cursor-deeplink/mcp/install?name=tapp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBhYXJ3aXR6L3RhcHBAbGF0ZXN0IiwibWNwIl19)
10
10
  [![VS Code MCP](https://img.shields.io/badge/VS_Code-Install_MCP-0098FF)](https://insiders.vscode.dev/redirect/mcp/install?name=tapp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40aarwitz%2Ftapp%40latest%22%2C%22mcp%22%5D%7D)
11
11
 
12
- **Tapp is the release-contract and evidence layer for teams shipping agent-authored applications.**
13
- It turns a repository and real product into an observed UI Map, a compact reviewed deterministic
14
- suite, and an inspectable merge decision.
15
-
16
- Coding agents can write the code, and (with Playwright & friends) they can even drive the app.
17
- What nobody gives them is **proof it works**. tapp explores your app like a user — no test code,
18
- no app changes — and surfaces what's broken, with evidence. Then a deterministic gate applies
19
- versioned policy to that evidence — the findings and coverage, any selected deterministic suites
20
- (Flows/Scenarios/contracts), and, when available, a target-scoped baseline — and returns a merge
21
- decision your queue can trust: `pass`, `fail`, or `inconclusive`. Exploration **observes**; the gate
22
- **judges** never a soft "ship-ready" guess.
23
-
24
- Three platforms, one observe-and-gate engine:
25
-
26
- - **iOS** the missing Playwright for iOS. tapp is hands *and* eyes: a generic XCUITest
27
- harness drives any app on the simulator via the accessibility surface. Native — no Appium,
28
- no WebDriverAgent.
29
- - **Android** black-box native driving through ADB + UIAutomator. Install an APK, target its
30
- application id, and run the same exploration, committed Flows, evidence, and regression gate. The
31
- app does not link a Tapp SDK.
32
- - **Web (beta)** — built *on* Playwright. Your agent already has browser hands; tapp adds the
33
- autonomous exploration, the deterministic detectors (uncaught exceptions, failed requests,
34
- dead buttons, broken links, placeholder `href="#"` links, error pages), and the same gate.
12
+ **Tapp gives coding agents hands and eyes on real iOS, Android, and web apps.** It can inspect and
13
+ drive screens, explore for technical failures, save journeys as deterministic tests, and gate
14
+ reviewed behavior in CI.
15
+
16
+ Exploration reports findings, coverage, evidence, and limits. Only the repository-connected gate
17
+ returns `pass`, `fail`, or `inconclusive`. Tapp does not turn an autonomous crawl into a subjective
18
+ "ship-ready" score.
19
+
20
+ Supported targets:
21
+
22
+ | Target app | Runtime |
23
+ |---|---|
24
+ | iOS | Simulator on macOS with Xcode; driven through XCUITest and accessibility |
25
+ | Android | Connected emulator or device with `adb`; driven through UIAutomator |
26
+ | Web (beta) | Owned browser app in Playwright Chromium |
27
+
28
+ Windows can host Android and web testing when their prerequisites are installed. Windows desktop
29
+ UI applications such as WinForms, WPF, and WinUI are not currently Tapp targets.
35
30
 
36
31
  ## Give Tapp to your coding agent
37
32
 
@@ -41,38 +36,41 @@ After setup, the whole user prompt is:
41
36
 
42
37
  The official skill teaches the agent to choose the smallest useful operation, handle repositories
43
38
  with multiple app targets, inspect visual evidence, and keep exploration observations separate from
44
- release judgment.
39
+ gate decisions.
45
40
 
46
- **Claude Code skill and MCP tools together:**
41
+ **Claude Code, Codex, Cursor, Copilot, and other Agent Skills clients (recommended):**
47
42
 
48
43
  ```bash
49
- claude plugin marketplace add aarwitz/tapp
50
- claude plugin install tapp@tapp
44
+ npx -y skills add aarwitz/tapp --skill tapp
51
45
  ```
52
46
 
53
- Restart Claude Code after installation, open the application repository, and use the short prompt
54
- above. The plugin installs the `tapp` Agent Skill and launches the matching version of Tapp's stdio
55
- MCP server through npm; no additional prompt block, MCP configuration, or bundle id is required.
47
+ This installs the open Agent Skills workflow into the current project and lets the agent run the npm
48
+ CLI directly; no MCP server, plugin, account, API key, global Tapp install, or pasted prompt block is
49
+ required. Add `-g` for a user-wide install, or `--agent claude-code`, `--agent codex`, and similar
50
+ selectors to constrain the clients. Start or restart the agent from the application repository and
51
+ use the short prompt above.
56
52
 
57
- **Claude, Codex, Cursor, Copilot, and other Agent Skills clients — skill only:**
53
+ **Claude Code optional enhanced skill and MCP tools:**
58
54
 
59
55
  ```bash
60
- npx -y skills add aarwitz/tapp --skill tapp
56
+ claude plugin marketplace add aarwitz/tapp
57
+ claude plugin install tapp@tapp
61
58
  ```
62
59
 
63
- This installs the open Agent Skills version of the same instructions into the current project and
64
- lets the agent fall back to the npm CLI when MCP is not connected. Add `-g` for a user-wide install,
65
- or `--agent claude-code`, `--agent codex`, and similar selectors to constrain the clients.
60
+ The plugin bundles the same `tapp` Agent Skill with the matching npm-backed MCP server. Use it when
61
+ you want inline screenshot tool results and a persistent interactive tap/read/type session; it is
62
+ not required for the core skill-to-CLI workflow.
66
63
 
67
- **No agent integration:** run the npm package directly from an app repository:
64
+ **No agent integration:** run the npm package directly from an app repository in one line:
68
65
 
69
66
  ```bash
70
67
  npx -y @aarwitz/tapp@latest init . --explore
71
68
  ```
72
69
 
73
70
  **VS Code:** install [Tapp from the Marketplace](https://marketplace.visualstudio.com/items?itemName=lidi-solutions.tapp).
74
- It contributes the same cross-platform Agent Skill to Copilot plus focused iOS simulator tools and a
75
- live simulator panel. Android and web remain available through the skill's CLI/MCP workflow.
71
+ It contributes the same cross-platform Agent Skill to Copilot plus focused iOS simulator tools and
72
+ an auto-refreshing screenshot preview. The preview is not an embedded Simulator or video stream.
73
+ Android and web remain available through the skill's CLI/MCP workflow.
76
74
 
77
75
  ```
78
76
  you: "Add a logout button to the settings screen"
@@ -96,7 +94,9 @@ npx -y @aarwitz/tapp@latest explore # explores the real app; prints findings
96
94
  Claude Code can read the saved image with its file-reading tool; Codex can open it with
97
95
  `view_image`. The agent should report what the screenshot proves, relay the exploration findings
98
96
  as-is (an observation, not a merge decision — `tapp ci` gates that), and link the HTML evidence
99
- report. No server, account, config file, test code, API key, or bundle id is required for this loop.
97
+ report. No Tapp server, account, global install, API key, or bundle id is required for this loop.
98
+ The platform runtime still needs to be installed, and repository onboarding can write reviewed
99
+ configuration under `.tapp/`.
100
100
 
101
101
  The product, executable, and package leaf are all Tapp: npm distributes it as
102
102
  `@aarwitz/tapp`, while the installed command remains `tapp`.
@@ -171,18 +171,24 @@ npx -y @aarwitz/tapp@latest explore path/to/app-debug.apk --platform android --a
171
171
  npx -y @aarwitz/tapp@latest open com.acme.app --platform android
172
172
  ```
173
173
 
174
+ Turn a driven journey into a deterministic test, or print the complete Flow format before writing
175
+ one by hand:
176
+
177
+ ```bash
178
+ npx -y @aarwitz/tapp@latest flow example
179
+ npx -y @aarwitz/tapp@latest flow validate .tapp/flows/smoke.yml
180
+ npx -y @aarwitz/tapp@latest flow run .tapp/flows/smoke.yml
181
+ ```
182
+
183
+ Flows support an atomic `login` step using `$TEST_EMAIL` / `$TEST_PASSWORD`; Tapp's session recorder
184
+ creates that step without writing credential values into the repository.
185
+
174
186
  Optional but recommended (prebuilds the test harness so the first run is fast):
175
187
  ```bash
176
188
  npx -y @aarwitz/tapp@latest install # ~2 min, one time
177
189
  npx -y @aarwitz/tapp@latest doctor # verify Xcode / simulators / toolchain
178
190
  ```
179
191
 
180
- ### Optional browser workspace
181
-
182
- `npx -y @aarwitz/tapp@latest app .` opens a local Release Studio for people who want visual repository
183
- onboarding, release-plan review, and CI preparation. It is not required for the coding-agent
184
- `open`/`explore` workflow.
185
-
186
192
  ### MCP hookup (optional)
187
193
 
188
194
  The MCP server adds the two things a CLI can't do: **screenshots inline in your agent's
@@ -387,12 +393,6 @@ environment. Add
387
393
  [`docs/scenarios.md`](docs/scenarios.md). Automatic
388
394
  baselines are isolated by platform and target, so two same-platform apps are never compared.
389
395
 
390
- **The hosted service is under development and is not currently offered for customer repositories.**
391
- Do not upload private code or credentials to an old preview. The retained
392
- cloud prototype is not the production SaaS boundary. Use the local Release Studio and the portable
393
- GitHub Action in infrastructure you control until the new account, tenant authorization, private
394
- evidence, and isolated-worker boundary passes security review.
395
-
396
396
  ## Make your repo agent-verified
397
397
 
398
398
  Drop this into your repo's `AGENTS.md` (read by Codex, Cursor, Copilot, Devin, Zed, …) so
@@ -435,13 +435,6 @@ only an optional authoring/enrichment layer (`tapp_flow_generate`, `assert_ai`,
435
435
  The first tool call builds the harness once (~2 min, cached in `~/.tapp`; rebuilt automatically
436
436
  if you switch simulators). All captures land in `~/.tapp/captures/`.
437
437
 
438
- ## Desktop status
439
-
440
- The macOS cockpit is frozen as a supported native interface and parity floor; it still reads the
441
- canonical Application Model, release plan, and UI Map in Coverage. Its older import/build path is
442
- not yet a thin client of the shared product-operation layer, so new product work is converging in
443
- the browser without deleting or reducing the desktop experience.
444
-
445
438
  ## License
446
439
 
447
440
  [MIT](./LICENSE)
package/bin/tapp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // tapp CLI — ship with proof.
2
+ // Tapp CLI — agent-driven app testing on real app surfaces.
3
3
  //
4
4
  // Zero-config verbs (the same engine the MCP tools use, exported by mcp-server/src/index.js):
5
5
  // tapp explore <bundleId|appId|url> Autonomous exploration → findings + evidence (observation)
@@ -53,6 +53,19 @@ function bad(label, detail = "") {
53
53
  console.log(` ❌ ${label}${detail ? ` — ${detail}` : ""}`);
54
54
  }
55
55
 
56
+ let lastProgressLine = "";
57
+ function writeProgress(line) {
58
+ const text = String(line || "").trimEnd();
59
+ if (process.stderr.isTTY) process.stderr.write(`\r${text} `);
60
+ else if (text && text !== lastProgressLine) console.error(text);
61
+ lastProgressLine = text;
62
+ }
63
+
64
+ function finishProgress() {
65
+ if (process.stderr.isTTY && lastProgressLine) process.stderr.write("\n");
66
+ lastProgressLine = "";
67
+ }
68
+
56
69
  function bootedSims() {
57
70
  const r = run("xcrun", ["simctl", "list", "devices", "booted", "-j"]);
58
71
  if (r.code !== 0) return [];
@@ -155,7 +168,7 @@ const engineImport = () => import(path.join(packageRoot, "mcp-server", "src", "i
155
168
 
156
169
  function requireMacFor(what) {
157
170
  if (process.platform === "darwin") return;
158
- console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp explore https://localhost:3000`);
171
+ console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: npx -y @aarwitz/tapp@latest explore https://localhost:3000`);
159
172
  process.exit(1);
160
173
  }
161
174
 
@@ -247,7 +260,7 @@ function safeCommandUsage(verb) {
247
260
  shot: "tapp shot [--out FILE]",
248
261
  apps: "tapp apps",
249
262
  build: "tapp build [repo] [--scheme NAME] [--configuration NAME]",
250
- flow: "tapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
263
+ flow: "tapp flow example\ntapp flow validate FILE [--platform PLATFORM] [--map FILE]\ntapp flow run FILE [--email VALUE] [--password VALUE]",
251
264
  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]",
252
265
  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]",
253
266
  scenario: "tapp scenario validate FILE [--project-dir DIR]\ntapp scenario run FILE --platform web --url URL [--project-dir DIR]",
@@ -279,9 +292,8 @@ const safeHelpRequested = (rest.includes("--help") || rest.includes("-h"))
279
292
  && !["help", "version", "--version", "-v"].includes(command)
280
293
  && (command !== "ci" || rest[0] === "install");
281
294
  if (safeHelpRequested) {
282
- console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens. Full command reference:\n`);
283
- command = "help";
284
- rest = [];
295
+ console.log(`Usage:\n ${safeCommandUsage(command).replaceAll("\n", "\n ")}\n\nℹ️ --help never builds, launches, writes, or opens. Run \`tapp --help\` for the full command reference.`);
296
+ process.exit(0);
285
297
  }
286
298
 
287
299
  // Create TAPP_HOME only for commands that actually use it — never for help/version/--help.
@@ -350,7 +362,7 @@ switch (command) {
350
362
  runExploration: engine?.runInitExploration,
351
363
  onProgress: (progress) => {
352
364
  const activePlatform = progress.platform || platform;
353
- process.stderr.write(`\r🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${activePlatform === "web" ? "pages reached" : activePlatform === "ios" ? "structural states observed" : "screens reached"} `);
365
+ writeProgress(`🔍 Import exploration… ${progress.action}/${progress.max || actions} actions · ${progress.states} ${activePlatform === "web" ? "pages reached" : activePlatform === "ios" ? "structural states observed" : "screens reached"}`);
354
366
  },
355
367
  onStatus: (status) => console.error(`⏳ ${status}`),
356
368
  outDir,
@@ -377,11 +389,11 @@ switch (command) {
377
389
  }
378
390
  }
379
391
  if (failure) {
380
- if (explore) process.stderr.write("\n");
392
+ if (explore) finishProgress();
381
393
  printEngineError({ error: `Could not initialize repository: ${failure.message || String(failure)}`, details: failure.details || {} });
382
394
  process.exit(2);
383
395
  }
384
- if (explore) process.stderr.write("\n");
396
+ if (explore) finishProgress();
385
397
  const built = { model: result.model, plan: result.plan };
386
398
  const written = result.written;
387
399
  const exploration = result.exploration;
@@ -547,7 +559,7 @@ switch (command) {
547
559
  const modelPlatform = typeof flags.platform === "string" ? flags.platform.toLowerCase() : "";
548
560
  if (modelPlatform === "ios") requireMacFor("iOS testing");
549
561
  const onProgress = (p) =>
550
- process.stderr.write(`\r🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} states observed `);
562
+ writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} states observed`);
551
563
  const r = await engine.runExploreTarget({
552
564
  projectDir: process.cwd(),
553
565
  platform: modelPlatform,
@@ -563,7 +575,7 @@ switch (command) {
563
575
  onProgress,
564
576
  onStatus: (t) => console.error(`ℹ️ ${t}`),
565
577
  });
566
- process.stderr.write("\n");
578
+ finishProgress();
567
579
  if (r.error) { printEngineError(r); process.exit(1); }
568
580
  console.log(r.text);
569
581
  if (flags.json && typeof flags.json === "string") {
@@ -588,14 +600,14 @@ switch (command) {
588
600
  process.exit(2);
589
601
  }
590
602
  if (platform === "web" && !/^https?:\/\//i.test(target)) {
591
- console.error("❌ Web QA needs an http(s) URL");
603
+ console.error("❌ Web exploration needs an http(s) URL");
592
604
  process.exit(2);
593
605
  }
594
606
  const bundleId = platform === "ios" ? await resolveTargetOrExit(engine, target) : null;
595
607
  const android = platform === "android" ? androidTarget(flags, target) : null;
596
608
  const progressMetric = platform === "web" ? "pages reached" : platform === "ios" ? "structural states observed" : "screens reached";
597
609
  const onProgress = (p) =>
598
- process.stderr.write(`\r🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${progressMetric} `);
610
+ writeProgress(`🔍 Exploring… ${p.action}/${p.max || flags.actions || 60} actions · ${p.states} ${progressMetric}`);
599
611
  const r = platform === "web"
600
612
  ? await engine.runQaWeb({
601
613
  url: target,
@@ -628,7 +640,7 @@ switch (command) {
628
640
  surface: "cli",
629
641
  onProgress,
630
642
  });
631
- process.stderr.write("\n");
643
+ finishProgress();
632
644
  if (r.error) {
633
645
  printEngineError(r);
634
646
  process.exit(1);
@@ -839,8 +851,22 @@ switch (command) {
839
851
  printEngineError(inst);
840
852
  process.exit(1);
841
853
  }
854
+ let modelRefresh = null;
855
+ try {
856
+ const { persistIosBuildValidation } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
857
+ modelRefresh = await persistIosBuildValidation({
858
+ projectDir: dir,
859
+ bundleId: inst.bundleId,
860
+ container: built.container,
861
+ scheme: built.scheme,
862
+ configuration: built.configuration,
863
+ });
864
+ } catch (error) {
865
+ console.error(`⚠️ Build succeeded, but Tapp could not refresh the existing application model: ${error.message || String(error)}`);
866
+ }
842
867
  console.log(`🔨 Built ${path.basename(built.appPath)} (scheme ${built.scheme}) — installed as ${inst.bundleId}`);
843
- console.log(`\nNext: tapp explore ${inst.bundleId}`);
868
+ if (modelRefresh) console.log(` application model refreshed: ${modelRefresh.modelPath}`);
869
+ console.log(`\nNext: npx -y @aarwitz/tapp@latest explore ${inst.bundleId}`);
844
870
  break;
845
871
  }
846
872
 
@@ -923,8 +949,12 @@ switch (command) {
923
949
  const { flags, positionals } = parseVerbArgs(rest);
924
950
  const verb = positionals[0] || "run";
925
951
  const flowPath = positionals[1] || (verb === "run" || verb === "validate" ? "" : verb);
952
+ if (verb === "example") {
953
+ console.log(`# Tapp Flow — deterministic, keyless replay\nname: sign-in-smoke\nplatform: web\nurl: https://example.test/login\nsteps:\n - login:\n email: $TEST_EMAIL\n password: $TEST_PASSWORD\n - wait_for: Dashboard\n - assert_screen: Dashboard\n`);
954
+ break;
955
+ }
926
956
  if (!["run", "validate"].includes(verb) || !flowPath) {
927
- console.error("usage: tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
957
+ console.error("usage: tapp flow example\n tapp flow run <flow.yml> [--platform ios|android|web] [--url URL] [--app-id ID] [--apk FILE] [--serial ID]\n tapp flow validate <flow.yml>");
928
958
  process.exit(2);
929
959
  }
930
960
  const absolute = path.resolve(flowPath);
@@ -1054,7 +1084,7 @@ switch (command) {
1054
1084
  try {
1055
1085
  const adopted = adoptPrCoverageProposal({ projectDir, prPlanPath, item: flags.item, releasePlanPath: typeof flags["release-plan"] === "string" ? flags["release-plan"] : undefined });
1056
1086
  console.log(`📥 ${adopted.mode === "reconciled-existing" ? "Reconciled PR evidence into" : "Adopted"} ${adopted.item.name}${adopted.mode === "reconciled-existing" ? ` while preserving decision '${adopted.item.decision}'` : " as a pending release-plan item"}; no Task or contract was generated or trusted`);
1057
- console.log(` plan: ${adopted.path}\n next: tapp plan review ${adopted.path} --approve ${adopted.item.id}`);
1087
+ console.log(` plan: ${adopted.path}\n next: npx -y @aarwitz/tapp@latest plan review ${adopted.path} --approve ${adopted.item.id}`);
1058
1088
  } catch (error) { console.error(`❌ Could not adopt PR coverage proposal: ${error.message || String(error)}`); process.exit(2); }
1059
1089
  break;
1060
1090
  }
@@ -1214,6 +1244,13 @@ switch (command) {
1214
1244
  const python = run("python3", ["--version"]);
1215
1245
  python.code === 0 ? ok("python3", `${python.stdout} (used by Flows)`) : bad("python3", "not found — Flow replay needs python3 + pyyaml (everything else works)");
1216
1246
 
1247
+ const { storagePreflight } = await import(path.join(packageRoot, "mcp-server", "src", "environment-preflight.js"));
1248
+ const storage = storagePreflight(tappHome);
1249
+ if (storage.level === "blocked") { bad("Disk space", storage.message); healthy = false; }
1250
+ else if (storage.level === "warning") console.log(` ⚠️ Disk space — ${storage.message}`);
1251
+ else if (storage.level === "ok") ok("Disk space", storage.message);
1252
+ else console.log(` ⬜ Disk space — ${storage.message || "could not be checked"}`);
1253
+
1217
1254
  console.log("\n Platforms:");
1218
1255
  if (process.platform === "darwin") {
1219
1256
  const xcode = run("xcode-select", ["-p"]);
@@ -1223,7 +1260,7 @@ switch (command) {
1223
1260
  const booted = bootedSims();
1224
1261
  ok("iOS", `${ver || "Xcode"}; ${booted.length ? `${booted[0].name} booted` : "no simulator booted yet"}`);
1225
1262
  const xctestrun = harnessXctestrun();
1226
- xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: tapp install)");
1263
+ xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: npx -y @aarwitz/tapp@latest install)");
1227
1264
  } else {
1228
1265
  console.log(" ⬜ iOS — unavailable (install Xcode + simulator runtime)");
1229
1266
  }
@@ -1257,7 +1294,7 @@ switch (command) {
1257
1294
  console.log(`\n Home: ${tappHome}`);
1258
1295
  console.log(healthy
1259
1296
  ? "\nReady. Start with:\n npx -y @aarwitz/tapp@latest open [target]\n npx -y @aarwitz/tapp@latest explore [target]"
1260
- : "\nFix the ❌ items above, then re-run: tapp doctor");
1297
+ : "\nFix the ❌ items above, then re-run: npx -y @aarwitz/tapp@latest doctor");
1261
1298
  process.exit(healthy ? 0 : 1);
1262
1299
  }
1263
1300
 
@@ -1332,7 +1369,13 @@ switch (command) {
1332
1369
  console.log(`✅ Actor '${name}' configured — ${result.actor.session} session · ${result.actor.provisioning} provisioning`);
1333
1370
  console.log(` ${result.path}`);
1334
1371
  console.log(` bindings: ${Object.entries(result.actor.credentials).map(([key, binding]) => `${key}=$${binding.env}`).join(", ") || "none"}`);
1335
- console.log(" No credential values were accepted or written. Rerun tapp init --refresh to update the application model.");
1372
+ const { refreshExistingInitArtifacts } = await import(path.join(packageRoot, "mcp-server", "src", "application-model.js"));
1373
+ let refreshed = null;
1374
+ let refreshWarning = "";
1375
+ try { refreshed = await refreshExistingInitArtifacts({ projectDir }); }
1376
+ catch (error) { refreshWarning = error.message || String(error); }
1377
+ console.log(` No credential values were accepted or written.${refreshed ? ` Application model refreshed: ${refreshed.modelPath}` : " Run npx -y @aarwitz/tapp@latest init when you are ready to create the application model."}`);
1378
+ if (refreshWarning) console.error(`⚠️ Actor was saved, but Tapp could not refresh the existing application model: ${refreshWarning}`);
1336
1379
  } catch (error) { console.error(`❌ Actor not configured: ${error.message || String(error)}`); process.exit(2); }
1337
1380
  break;
1338
1381
  }
@@ -1347,7 +1390,7 @@ switch (command) {
1347
1390
  const projectDir = fs.realpathSync(path.resolve(positionals[1] || (typeof flags["project-dir"] === "string" ? flags["project-dir"] : process.cwd())));
1348
1391
  const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
1349
1392
  if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
1350
- console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore, review/generate/validate/promote the plan, then create the baseline.`);
1393
+ console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore, review/generate/validate/promote the plan, then create the baseline.`);
1351
1394
  process.exit(2);
1352
1395
  }
1353
1396
  let model;
@@ -1454,7 +1497,7 @@ switch (command) {
1454
1497
  catch { console.error(`❌ Repository directory not found: ${positionals[0] || flags["project-dir"] || process.cwd()}`); process.exit(2); }
1455
1498
  const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
1456
1499
  if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
1457
- console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore first.`);
1500
+ console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore first.`);
1458
1501
  process.exit(2);
1459
1502
  }
1460
1503
  const actionRef = typeof flags["action-ref"] === "string" ? flags["action-ref"] : `aarwitz/tapp@v${pkg.version}`;
@@ -1571,17 +1614,17 @@ switch (command) {
1571
1614
  }
1572
1615
 
1573
1616
  default: {
1574
- console.log(`tapp v${pkg.version} — ship with proof. Autonomous exploration and deterministic release gates for iOS, Android, and web.
1617
+ console.log(`tapp v${pkg.version} — agent-driven app testing for iOS, Android, and web.
1575
1618
 
1576
- Core — explore, prove, gate (agents and humans can just run these — no server, no setup):
1619
+ Core — inspect, explore, gate (no Tapp account or server required):
1577
1620
  tapp explore [target] Autonomous exploration → findings + evidence (an observation, NOT a
1578
- release decision — run 'tapp ci' to gate a merge)
1621
+ release decision — run 'npx -y @aarwitz/tapp@latest ci' to gate a merge)
1579
1622
  (web: --watch · all: --platform ios|android|web · --actions N)
1580
1623
  tapp contract run FILE Replay a business-level release contract — the guarantees that must hold
1581
1624
  tapp ci ... Merge-blocking release gate — explore + suites + baseline → pass/fail/inconclusive
1582
1625
  (see: tapp ci --help)
1583
1626
 
1584
- Primitives — an agent's eyes and hands (no setup):
1627
+ Primitives — an agent's eyes and hands:
1585
1628
  tapp open [target] Launch the app → screen summary + screenshot saved to a file
1586
1629
  (web: --tap TEXT · --wait-for TEXT · --out FILE)
1587
1630
  tapp tree [target] Accessibility tree of the current screen (--json for every element)
@@ -1597,6 +1640,7 @@ Repository & release:
1597
1640
  tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
1598
1641
 
1599
1642
  Advanced — deterministic suites, lifecycle & compilers:
1643
+ tapp flow example Print a complete starter Flow YAML (no target or MCP required)
1600
1644
  tapp flow run FILE Replay a committed deterministic Flow (no AI/API key)
1601
1645
  tapp flow validate FILE Validate a Flow without launching a target
1602
1646
  tapp task validate FILE Validate a reusable deterministic Task (+ optional UI Map grounding)
@@ -1636,7 +1680,7 @@ Setup:
1636
1680
  tapp doctor Check Xcode / simulators / toolchain
1637
1681
  tapp mcp Start the MCP server on stdio (adds inline screenshots + interactive sessions)
1638
1682
 
1639
- Agent Skill (optional — so a short “Use Tapp to test this app” prompt is enough):
1683
+ Agent Skill (recommended — so a short “Use Tapp to test this app” prompt is enough):
1640
1684
  Any supported agent: npx -y skills add aarwitz/tapp --skill tapp
1641
1685
  Claude skill + MCP: claude plugin marketplace add aarwitz/tapp
1642
1686
  claude plugin install tapp@tapp
@@ -74,6 +74,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
74
74
  const visited = new Map();
75
75
  let issues = 0;
76
76
  let actions = 0;
77
+ let loginTried = false;
77
78
  const crashExitBaseline = typeof d.latestCrashExitInfo === "function" ? await d.latestCrashExitInfo() : null;
78
79
  let snap = await d.launch({ clearData });
79
80
  let crashReported = false;
@@ -210,6 +211,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
210
211
  if (candidate) {
211
212
  const target = controlLabel(candidate);
212
213
  const loginSubmit = inputs.some((input) => input.secure) && isAndroidAuthSubmit(candidate);
214
+ if (loginSubmit) loginTried = true;
213
215
  tried.add(`${hash}|tap|${target}`);
214
216
  const before = hash;
215
217
  const r = await d.tap(target, snap);
@@ -260,7 +262,7 @@ export async function exploreAndroid({ appId, apkPath, serial, maxActions = 40,
260
262
  }
261
263
 
262
264
  const timedOut = Date.now() >= deadline;
263
- emit("COMPLETE", { actions, states: visited.size, issues, screens: [...new Set(visited.values())].join(","), outcome: timedOut ? "timeout" : "complete", timedOut, ...(timedOut ? { timeoutSeconds: timeoutSec } : {}) });
265
+ emit("COMPLETE", { actions, states: visited.size, issues, screens: [...new Set(visited.values())].join(","), outcome: timedOut ? "timeout" : "complete", timedOut, credentialsProvided: !!(testEmail || testPassword), credentialsUsed: loginTried, ...(timedOut ? { timeoutSeconds: timeoutSec } : {}) });
264
266
  onProgress({ action: actions, max: maxActions, states: visited.size });
265
267
  return { markersPath, outDir, actions, states: visited.size, issues, timedOut, seedTargets: normalizedTargets };
266
268
  }