@aarwitz/tapp 0.17.0-rc.11 → 0.17.0-rc.13
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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +33 -0
- package/AGENTS.md +12 -9
- package/README.md +81 -34
- package/bin/tapp.js +18 -6
- package/docs/BROWSER-PRODUCT.md +1 -1
- package/mcp-server/src/index.js +165 -77
- package/mcp-server/src/product-operations.js +2 -1
- package/mcp-server/src/web-explorer.js +120 -8
- package/package.json +4 -2
- package/skills/tapp/SKILL.md +74 -0
- package/skills/tapp/agents/openai.yaml +4 -0
- package/skills/tapp/references/commands.md +102 -0
|
@@ -23,6 +23,7 @@ import { execFileSync } from "child_process";
|
|
|
23
23
|
const CLICK_SETTLE_MS = 700;
|
|
24
24
|
const NAV_TIMEOUT_MS = 15_000;
|
|
25
25
|
const BUTTONS_PER_PAGE = 4;
|
|
26
|
+
const WATCH_ACTION_DELAY_MS = 350;
|
|
26
27
|
const ERROR_TEXT_RE = /\b(something went wrong|internal server error|an error occurred|failed to load|unhandled exception)\b/i;
|
|
27
28
|
const STANDALONE_ERROR_TEXT_RE = /^(something went wrong|internal server error|an error occurred|failed to load|unhandled exception)(?:[.!:]|\s|$)/i;
|
|
28
29
|
|
|
@@ -249,13 +250,14 @@ export async function loadPlaywright() {
|
|
|
249
250
|
);
|
|
250
251
|
}
|
|
251
252
|
|
|
252
|
-
export async function submitWebLogin(page) {
|
|
253
|
+
export async function submitWebLogin(page, beforeClick = null) {
|
|
253
254
|
const candidates = [
|
|
254
255
|
page.locator("button[type=submit], input[type=submit], form button").first(),
|
|
255
256
|
page.getByRole("button", { name: /sign ?in|log ?in|continue/i }).first(),
|
|
256
257
|
];
|
|
257
258
|
for (const candidate of candidates) {
|
|
258
259
|
if (await candidate.isVisible().catch(() => false)) {
|
|
260
|
+
if (beforeClick) await beforeClick(candidate);
|
|
259
261
|
await candidate.click({ timeout: 3000 });
|
|
260
262
|
return true;
|
|
261
263
|
}
|
|
@@ -279,13 +281,14 @@ export function webTransitionOrigin(pendingNavigation, currentScreen) {
|
|
|
279
281
|
return pendingNavigation?.fromScreen || currentScreen || null;
|
|
280
282
|
}
|
|
281
283
|
|
|
282
|
-
export function webBrowserLaunchOptions(environment = process.env) {
|
|
284
|
+
export function webBrowserLaunchOptions(environment = process.env, { watch = false } = {}) {
|
|
283
285
|
const browserProxy = String(environment.TAPP_BROWSER_PROXY_SERVER || "").trim();
|
|
284
286
|
if (environment.TAPP_ENFORCE_PUBLIC_EGRESS === "1" && !/^http:\/\/127\.0\.0\.1:\d+$/.test(browserProxy)) {
|
|
285
287
|
throw new Error("public egress policy proxy is required");
|
|
286
288
|
}
|
|
287
289
|
return {
|
|
288
|
-
headless:
|
|
290
|
+
headless: !watch,
|
|
291
|
+
...(watch ? { slowMo: 200 } : {}),
|
|
289
292
|
...(browserProxy ? { proxy: { server: browserProxy, bypass: "<-loopback>" } } : {}),
|
|
290
293
|
args: browserProxy ? [
|
|
291
294
|
"--disable-quic",
|
|
@@ -296,6 +299,102 @@ export function webBrowserLaunchOptions(environment = process.env) {
|
|
|
296
299
|
};
|
|
297
300
|
}
|
|
298
301
|
|
|
302
|
+
// A headed Playwright browser does not move the host OS pointer when locator.click() runs. In
|
|
303
|
+
// explicit watch mode, draw a pointer inside the controlled page so a human can follow Tapp's
|
|
304
|
+
// real actions. The UI lives in a closed shadow root, ignores pointer events, and is hidden from
|
|
305
|
+
// evidence screenshots; it therefore cannot become an app control or alter detector input.
|
|
306
|
+
async function installWebWatchUi(context) {
|
|
307
|
+
await context.addInitScript(() => {
|
|
308
|
+
const stateKey = Symbol.for("tapp.watchUi");
|
|
309
|
+
const ensure = () => {
|
|
310
|
+
if (window[stateKey]?.host?.isConnected) return window[stateKey];
|
|
311
|
+
const host = document.createElement("div");
|
|
312
|
+
host.setAttribute("data-tapp-watch-ui", "");
|
|
313
|
+
host.setAttribute("aria-hidden", "true");
|
|
314
|
+
Object.assign(host.style, {
|
|
315
|
+
position: "fixed",
|
|
316
|
+
inset: "0",
|
|
317
|
+
zIndex: "2147483647",
|
|
318
|
+
pointerEvents: "none",
|
|
319
|
+
});
|
|
320
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
321
|
+
const style = document.createElement("style");
|
|
322
|
+
style.textContent = `
|
|
323
|
+
.cursor { position: fixed; left: 24px; top: 72px; width: 22px; height: 28px;
|
|
324
|
+
filter: drop-shadow(0 2px 2px rgba(0,0,0,.45)); transition: left 260ms ease, top 260ms ease;
|
|
325
|
+
transform: rotate(-8deg); }
|
|
326
|
+
.cursor::before { content: ""; display: block; width: 100%; height: 100%; background: #111827;
|
|
327
|
+
clip-path: polygon(0 0, 0 88%, 25% 67%, 39% 100%, 53% 93%, 39% 61%, 70% 61%); }
|
|
328
|
+
.cursor::after { content: ""; position: absolute; inset: 2px 3px 4px 2px; background: white;
|
|
329
|
+
clip-path: polygon(0 0, 0 79%, 25% 59%, 40% 91%, 46% 88%, 32% 55%, 60% 55%); }
|
|
330
|
+
.hud { position: fixed; top: 14px; right: 14px; max-width: min(420px, calc(100vw - 28px));
|
|
331
|
+
box-sizing: border-box; padding: 9px 12px; border-radius: 10px; color: white;
|
|
332
|
+
background: rgba(17,24,39,.92); box-shadow: 0 5px 18px rgba(0,0,0,.24);
|
|
333
|
+
font: 600 13px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
334
|
+
.brand { color: #93c5fd; margin-right: 6px; }
|
|
335
|
+
`;
|
|
336
|
+
const cursor = document.createElement("div");
|
|
337
|
+
cursor.className = "cursor";
|
|
338
|
+
const hud = document.createElement("div");
|
|
339
|
+
hud.className = "hud";
|
|
340
|
+
shadow.append(style, cursor, hud);
|
|
341
|
+
(document.documentElement || document).appendChild(host);
|
|
342
|
+
const state = { host, cursor, hud };
|
|
343
|
+
Object.defineProperty(window, stateKey, { value: state, configurable: true });
|
|
344
|
+
return state;
|
|
345
|
+
};
|
|
346
|
+
Object.defineProperty(window, "__tappShowWatchAction", {
|
|
347
|
+
configurable: true,
|
|
348
|
+
value: ({ x, y, action, target }) => {
|
|
349
|
+
const state = ensure();
|
|
350
|
+
state.host.style.display = "block";
|
|
351
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
352
|
+
state.cursor.style.left = `${Math.max(4, Math.min(window.innerWidth - 26, x))}px`;
|
|
353
|
+
state.cursor.style.top = `${Math.max(4, Math.min(window.innerHeight - 32, y))}px`;
|
|
354
|
+
}
|
|
355
|
+
state.hud.replaceChildren();
|
|
356
|
+
const brand = document.createElement("span");
|
|
357
|
+
brand.className = "brand";
|
|
358
|
+
brand.textContent = "Tapp";
|
|
359
|
+
state.hud.append(brand, document.createTextNode(`${action}${target ? ` · ${target}` : ""}`));
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
Object.defineProperty(window, "__tappSetWatchUiVisible", {
|
|
363
|
+
configurable: true,
|
|
364
|
+
value: (visible) => {
|
|
365
|
+
if (window[stateKey]?.host) window[stateKey].host.style.display = visible ? "block" : "none";
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function showWebWatchAction(page, { locator = null, action = "Exploring", target = "" } = {}) {
|
|
372
|
+
let x = 28;
|
|
373
|
+
let y = 76;
|
|
374
|
+
if (locator) {
|
|
375
|
+
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
376
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
377
|
+
if (box) {
|
|
378
|
+
x = box.x + box.width / 2;
|
|
379
|
+
y = box.y + box.height / 2;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
await page.evaluate(({ x, y, action, target }) => {
|
|
383
|
+
window.__tappShowWatchAction?.({ x, y, action, target });
|
|
384
|
+
}, { x, y, action, target }).catch(() => {});
|
|
385
|
+
await page.waitForTimeout(WATCH_ACTION_DELAY_MS).catch(() => {});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function screenshotWithoutWebWatchUi(page, options, watch) {
|
|
389
|
+
if (!watch) return page.screenshot(options);
|
|
390
|
+
await page.evaluate(() => window.__tappSetWatchUiVisible?.(false)).catch(() => {});
|
|
391
|
+
try {
|
|
392
|
+
return await page.screenshot(options);
|
|
393
|
+
} finally {
|
|
394
|
+
await page.evaluate(() => window.__tappSetWatchUiVisible?.(true)).catch(() => {});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
299
398
|
// Focused one-screen inspection for the agent-facing `tapp open <url>` and `tapp tree <url>`
|
|
300
399
|
// commands. This deliberately does no exploration or judgment; it opens exactly one page,
|
|
301
400
|
// captures the visible semantic controls, and optionally takes one screenshot.
|
|
@@ -425,7 +524,7 @@ export function normalizeWebSeedTargets(seedTargets = [], limit = 5) {
|
|
|
425
524
|
return result;
|
|
426
525
|
}
|
|
427
526
|
|
|
428
|
-
export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDir, testEmail = "", testPassword = "", seedRoutes = [], seedTargets = [], onProgress }) {
|
|
527
|
+
export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDir, testEmail = "", testPassword = "", seedRoutes = [], seedTargets = [], watch = false, onProgress }) {
|
|
429
528
|
const start = new URL(url);
|
|
430
529
|
if (!/^https?:$/.test(start.protocol)) throw new Error("url must be http(s)");
|
|
431
530
|
fs.mkdirSync(outDir, { recursive: true });
|
|
@@ -434,9 +533,10 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
434
533
|
const emit = (kind, payload) => fs.writeSync(markersFd, `OCQA_${kind}:${JSON.stringify(payload)}\n`);
|
|
435
534
|
|
|
436
535
|
const { chromium } = await loadPlaywright();
|
|
437
|
-
const browser = await chromium.launch(webBrowserLaunchOptions());
|
|
536
|
+
const browser = await chromium.launch(webBrowserLaunchOptions(process.env, { watch }));
|
|
438
537
|
const context = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
439
538
|
await installWebListenerTracking(context);
|
|
539
|
+
if (watch) await installWebWatchUi(context);
|
|
440
540
|
const page = await context.newPage();
|
|
441
541
|
page.setDefaultTimeout(NAV_TIMEOUT_MS);
|
|
442
542
|
|
|
@@ -604,7 +704,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
604
704
|
if (existingKey && existingKey !== evidenceKey) screenshotFor.delete(existingKey);
|
|
605
705
|
screenshotFor.set(evidenceKey, { path: screenshotPath, busy: info.busy, route: key });
|
|
606
706
|
screenCount = screenshotFor.size;
|
|
607
|
-
await page
|
|
707
|
+
await screenshotWithoutWebWatchUi(page, { path: screenshotPath }, watch).catch(() => {});
|
|
608
708
|
// Deterministic per-page detectors run once per distinct screen.
|
|
609
709
|
if (webPageAppearsBlank(info)) issue("blank_screen", "high", "Page rendered no visible content", screen);
|
|
610
710
|
else {
|
|
@@ -628,12 +728,19 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
628
728
|
if (!(await pw.isVisible().catch(() => false))) return null;
|
|
629
729
|
loginTried = true;
|
|
630
730
|
const emailSel = "input[type=email], input[name*=mail i], input[name*=user i], input[id*=mail i], input[id*=user i]";
|
|
631
|
-
if (testEmail)
|
|
731
|
+
if (testEmail) {
|
|
732
|
+
const email = page.locator(emailSel).first();
|
|
733
|
+
if (watch) await showWebWatchAction(page, { locator: email, action: "Type", target: "Email" });
|
|
734
|
+
await email.fill(testEmail).catch(() => {});
|
|
735
|
+
}
|
|
736
|
+
if (watch) await showWebWatchAction(page, { locator: pw, action: "Type", target: "Password" });
|
|
632
737
|
await pw.fill(testPassword).catch(() => {});
|
|
633
738
|
lastActionTarget = "Sign in";
|
|
634
739
|
emit("ACTION", { type: "login", target: "Sign in", screen, narrative: "Filled and submitted the sign-in form with the provided test credentials" });
|
|
635
740
|
actions += 1;
|
|
636
|
-
await submitWebLogin(page
|
|
741
|
+
await submitWebLogin(page, watch
|
|
742
|
+
? (locator) => showWebWatchAction(page, { locator, action: "Click", target: "Sign in" })
|
|
743
|
+
: null).catch(() => false);
|
|
637
744
|
await waitForWebStability(page, { timeoutMs: Math.min(5_000, CLICK_SETTLE_MS * 6) });
|
|
638
745
|
// Still on the login form after a submit = the sign-in failed — full stop. (A quiet
|
|
639
746
|
// credential rejection often shows NO other symptom, so this must not be coupled to
|
|
@@ -688,6 +795,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
688
795
|
progress();
|
|
689
796
|
continue;
|
|
690
797
|
}
|
|
798
|
+
if (watch) await showWebWatchAction(page, { action: "Open", target });
|
|
691
799
|
await waitForWebStability(page);
|
|
692
800
|
if (nav && typeof nav.status === "function" && nav.status() === 404) {
|
|
693
801
|
issue("broken_link", "medium", `Broken link: ${target} → 404`, target);
|
|
@@ -708,6 +816,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
708
816
|
emit("ACTION", { type: action.type, target: action.target, screen: beforeScreen, reason: "pr_ui_map_path", narrative: `Following observed UI Map path: ${action.type} ${action.target}` });
|
|
709
817
|
let acted = false;
|
|
710
818
|
if (action.type === "back") {
|
|
819
|
+
if (watch) await showWebWatchAction(page, { action: "Back", target: beforeScreen });
|
|
711
820
|
await page.goBack({ waitUntil: "domcontentloaded", timeout: step.wait?.timeoutMs || NAV_TIMEOUT_MS }).catch(() => {});
|
|
712
821
|
acted = true;
|
|
713
822
|
} else {
|
|
@@ -721,6 +830,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
721
830
|
else continue;
|
|
722
831
|
if (await locator.isVisible().catch(() => false)) {
|
|
723
832
|
try {
|
|
833
|
+
if (watch) await showWebWatchAction(page, { locator, action: "Click", target: action.target });
|
|
724
834
|
await locator.click({ timeout: Math.min(step.wait?.timeoutMs || NAV_TIMEOUT_MS, NAV_TIMEOUT_MS) });
|
|
725
835
|
acted = true;
|
|
726
836
|
break;
|
|
@@ -785,6 +895,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
785
895
|
emit("ACTION", { type: "tap", target: label, screen: webActionScreen(ob), narrative: `Tapped "${label}"` });
|
|
786
896
|
let clickSucceeded = false;
|
|
787
897
|
try {
|
|
898
|
+
if (watch) await showWebWatchAction(page, { locator: b, action: "Click", target: label });
|
|
788
899
|
await b.click({ timeout: 3000 });
|
|
789
900
|
clickSucceeded = true;
|
|
790
901
|
} catch {}
|
|
@@ -795,6 +906,7 @@ export async function exploreWeb({ url, maxActions = 40, timeoutSec = 300, outDi
|
|
|
795
906
|
await waitForWebStability(page);
|
|
796
907
|
if (page.url() !== beforeState.url) {
|
|
797
908
|
await observe();
|
|
909
|
+
if (watch) await showWebWatchAction(page, { action: "Back", target: ob.screen });
|
|
798
910
|
await page.goBack({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
799
911
|
await waitForWebStability(page);
|
|
800
912
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aarwitz/tapp",
|
|
3
|
-
"version": "0.17.0-rc.
|
|
3
|
+
"version": "0.17.0-rc.13",
|
|
4
4
|
"mcpName": "io.github.aarwitz/tapp",
|
|
5
5
|
"description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"Harness/OCQAHarnessUITests/",
|
|
42
42
|
"Harness/OCQAHarness.xcodeproj/",
|
|
43
43
|
"Harness/generate-harness-xcodeproj.rb",
|
|
44
|
+
"skills/",
|
|
45
|
+
".claude-plugin/",
|
|
44
46
|
"AGENTS.md"
|
|
45
47
|
],
|
|
46
48
|
"dependencies": {
|
|
@@ -87,7 +89,7 @@
|
|
|
87
89
|
"mobile"
|
|
88
90
|
],
|
|
89
91
|
"scripts": {
|
|
90
|
-
"test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/android-primitives-protocol.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/action.test.js tests/package-surface.test.js tests/landing-brand.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js",
|
|
92
|
+
"test": "node --test tests/report.test.js tests/regression.test.js tests/engine.test.js tests/project-config.test.js tests/application-model.test.js tests/ui-map.test.js tests/task-runtime.test.js tests/release-contract.test.js tests/pr-selection.test.js tests/flow-runtime.test.js tests/web-explorer.test.js tests/web-session.test.js tests/web-flow.test.js tests/scenario-runtime.test.js tests/android-driver.test.js tests/android-explorer.test.js tests/android-flow.test.js tests/android-primitives-protocol.test.js tests/managed-web.test.js tests/product-operations.test.js tests/browser-product.test.js tests/browser-onboarding.test.js tests/managed-operation.test.js tests/cloud-runner.test.js tests/ci-setup.test.js tests/ci-install.test.js tests/cli.test.js tests/mcp-workspace.test.js tests/action.test.js tests/package-surface.test.js tests/agent-surface.test.js tests/landing-brand.test.js tests/ci-gate.test.js tests/ci-report.test.js tests/desktop-protocol.test.js tests/ios-flow-protocol.test.js vscode-extension/test/bridge.test.js",
|
|
91
93
|
"test:browser-journey": "node --test tests/browser-journey.test.js",
|
|
92
94
|
"test:browser-native": "TAPP_RUN_NATIVE_BROWSER=1 node --test tests/browser-native-journey.test.js"
|
|
93
95
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tapp
|
|
3
|
+
description: Use Tapp to see, drive, explore, and verify real application surfaces on iOS simulators, Android emulators/devices, or the web. Use when a user asks an agent to test an app or UI change, find bugs, inspect or screenshot a screen, exercise a journey, create a replayable flow, gather release evidence, or run the deterministic Tapp gate. Also use when the user mentions Tapp, @aarwitz/tapp, tapp_* tools, .tapp artifacts, or asks whether agent-authored UI actually works.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tapp
|
|
7
|
+
|
|
8
|
+
Use Tapp as the app's hands and eyes. Work on the real UI surface and show evidence; do not claim a
|
|
9
|
+
screen or journey works from source inspection alone.
|
|
10
|
+
|
|
11
|
+
## Choose the smallest operation
|
|
12
|
+
|
|
13
|
+
| Intent | Operation |
|
|
14
|
+
|---|---|
|
|
15
|
+
| See or screenshot one screen | `open` / `tapp_open_app` |
|
|
16
|
+
| Inspect controls on the current screen | `tree` / `tapp_ui_tree` |
|
|
17
|
+
| Drive a specific journey | MCP session start → act → end |
|
|
18
|
+
| Find bugs autonomously | `explore` / `tapp_explore` |
|
|
19
|
+
| Preserve a journey | record and save a Flow; replay it deterministically |
|
|
20
|
+
| Decide whether a merge passes policy | `ci`; exploration never decides this |
|
|
21
|
+
|
|
22
|
+
Prefer connected `tapp_*` MCP tools when available: they keep interactive sessions alive and return
|
|
23
|
+
screenshots inline. Otherwise run `npx -y @aarwitz/tapp@latest` from the app repository. Do not require MCP,
|
|
24
|
+
an account, an API key, or a global install for the core workflow.
|
|
25
|
+
|
|
26
|
+
## Start source-connected
|
|
27
|
+
|
|
28
|
+
When the user asks for a general first test of a repository:
|
|
29
|
+
|
|
30
|
+
1. If `.tapp/application-model.json` exists, run `npx -y @aarwitz/tapp@latest explore`.
|
|
31
|
+
2. Otherwise run `npx -y @aarwitz/tapp@latest init . --explore` or call `tapp_init` with
|
|
32
|
+
`{operation:"explore", projectDir:"."}`.
|
|
33
|
+
3. If Tapp returns `target-selection-required`, present its actual choices and ask the user to pick.
|
|
34
|
+
Never guess among multiple targets. Re-run with the selected platform/target exactly as Tapp
|
|
35
|
+
instructs.
|
|
36
|
+
4. If a prerequisite is missing, run `tapp doctor`, apply only the stated remediation that is in
|
|
37
|
+
scope, and retry once.
|
|
38
|
+
|
|
39
|
+
For a focused request, use the requested target directly rather than forcing repository onboarding.
|
|
40
|
+
Targets may be a repository path, Xcode container, `.app`, iOS bundle id, APK plus Android app id,
|
|
41
|
+
or owned HTTP(S) URL. Never explore a third-party web property without authorization: exploration
|
|
42
|
+
clicks and types.
|
|
43
|
+
|
|
44
|
+
## Observe honestly
|
|
45
|
+
|
|
46
|
+
Exploration returns findings, coverage, evidence, and `inconclusive`; it does not return a score or
|
|
47
|
+
ship verdict. Report:
|
|
48
|
+
|
|
49
|
+
- target and platform;
|
|
50
|
+
- screens/actions and whether coverage was conclusive;
|
|
51
|
+
- deterministic versus advisory finding counts;
|
|
52
|
+
- each important finding and its evidence/report path;
|
|
53
|
+
- what Tapp explicitly did not check.
|
|
54
|
+
|
|
55
|
+
If `inconclusive: true`, explain the blocker. A login wall or missing test data is not a pass. Ask for
|
|
56
|
+
credentials or launch configuration instead of rerunning blindly. Do not infer content accuracy,
|
|
57
|
+
privacy, brand consistency, or business guarantees from a generic crawl; those require a reviewed
|
|
58
|
+
Flow, Scenario, contract, verifier, or human review.
|
|
59
|
+
|
|
60
|
+
When a screenshot path is printed, open it with the client's image-reading tool before describing
|
|
61
|
+
the screen. For web, use `--watch` when the human wants to follow Tapp's controlled browser. For iOS,
|
|
62
|
+
point the human to the report's exploration recording when available.
|
|
63
|
+
|
|
64
|
+
## Drive safely
|
|
65
|
+
|
|
66
|
+
For an interactive MCP session, read returned `elements[]` before every action, target accessibility
|
|
67
|
+
ids or visible labels, check `hittable`, tap a field before typing, and wait for navigation or async
|
|
68
|
+
content. Use coordinates only as a last resort. End the session when finished.
|
|
69
|
+
|
|
70
|
+
Do not edit the app merely because testing found a defect unless the user also asked for a fix. State
|
|
71
|
+
what the evidence proves and what remains untested.
|
|
72
|
+
|
|
73
|
+
Read [references/commands.md](references/commands.md) only when exact CLI/MCP syntax, Flow replay,
|
|
74
|
+
credentials, or platform prerequisites are needed.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Tapp command reference
|
|
2
|
+
|
|
3
|
+
## Plain CLI
|
|
4
|
+
|
|
5
|
+
Run from the application repository. `[target]` is optional when Tapp can read the repository model
|
|
6
|
+
or detect one unambiguous target.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx -y @aarwitz/tapp@latest init . --explore
|
|
10
|
+
npx -y @aarwitz/tapp@latest explore [target]
|
|
11
|
+
npx -y @aarwitz/tapp@latest open [target]
|
|
12
|
+
npx -y @aarwitz/tapp@latest tree [target] --json
|
|
13
|
+
npx -y @aarwitz/tapp@latest shot
|
|
14
|
+
npx -y @aarwitz/tapp@latest report latest
|
|
15
|
+
npx -y @aarwitz/tapp@latest doctor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Platform examples:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Web: Tapp may build/start/stop a source target; --watch is human-visible.
|
|
22
|
+
npx -y @aarwitz/tapp@latest explore --platform web --target website --watch
|
|
23
|
+
npx -y @aarwitz/tapp@latest explore https://staging.example.com
|
|
24
|
+
|
|
25
|
+
# iOS: source repo, .app, or bundle id.
|
|
26
|
+
npx -y @aarwitz/tapp@latest explore MyApp.xcodeproj --platform ios
|
|
27
|
+
npx -y @aarwitz/tapp@latest open com.example.MyApp --platform ios
|
|
28
|
+
|
|
29
|
+
# Android: app id is required; APK is optional if already installed.
|
|
30
|
+
npx -y @aarwitz/tapp@latest explore app-debug.apk --platform android --app-id com.example.app
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Focused web evidence can perform one semantic interaction and wait for async content:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx -y @aarwitz/tapp@latest open https://example.com --tap "Not now" --wait-for "Dashboard"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## MCP mapping
|
|
40
|
+
|
|
41
|
+
- `tapp_init`: inspect or initialize a source repository; `operation:"explore"` prepares and explores.
|
|
42
|
+
- `tapp_build`: build and install an iOS app without needing its bundle id first.
|
|
43
|
+
- `tapp_open_app`: launch and return a screen summary plus inline screenshot.
|
|
44
|
+
- `tapp_ui_tree` / `tapp_screenshot`: inspect the current real surface.
|
|
45
|
+
- `tapp_session_start` → `tapp_session_act` → `tapp_session_end`: drive one persistent journey.
|
|
46
|
+
- `tapp_explore`: autonomous iOS, Android, or web exploration; observation only.
|
|
47
|
+
- `tapp_flow_save` / `tapp_flow_run`: save a driven journey and replay it deterministically.
|
|
48
|
+
- `tapp_release_contract`: validate, compile, or run a reviewed business guarantee.
|
|
49
|
+
- `tapp_ci_setup`: create a target-scoped baseline or reviewable CI installation.
|
|
50
|
+
|
|
51
|
+
An iOS no-bundle-id MCP path is `tapp_build {projectDir:"."}` followed by `tapp_explore` with the
|
|
52
|
+
returned `bundleId`. Android uses `androidAppId` and optional `apkPath`; web uses `url` or
|
|
53
|
+
source-connected `tapp_init`.
|
|
54
|
+
|
|
55
|
+
## Interactive session loop
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
tapp_session_start {appBundleId:"com.example.app"}
|
|
59
|
+
tapp_session_act {action:"tap", id:"Email"}
|
|
60
|
+
tapp_session_act {action:"type", text:"qa@example.com"}
|
|
61
|
+
tapp_session_act {action:"tap", id:"Password"}
|
|
62
|
+
tapp_session_act {action:"type", text:"..."}
|
|
63
|
+
tapp_session_act {action:"tap", id:"Sign In"}
|
|
64
|
+
tapp_session_act {action:"wait", text:"Home", timeoutMs:10000}
|
|
65
|
+
tapp_screenshot
|
|
66
|
+
tapp_session_end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Android sessions use `androidAppId`, optional `apkPath`, and the same action loop.
|
|
70
|
+
|
|
71
|
+
## Credentials and test configuration
|
|
72
|
+
|
|
73
|
+
For autonomous exploration, pass test-only values when authorized:
|
|
74
|
+
|
|
75
|
+
- CLI: `--email`, `--password`, repeated `--launch-arg`, and JSON `--launch-env`.
|
|
76
|
+
- MCP: `testEmail`, `testPassword`, `inputOverrides`, `appLaunchArgs`, `appLaunchEnv`, or explicit
|
|
77
|
+
`loginSteps`.
|
|
78
|
+
|
|
79
|
+
Do not persist secrets in `.tapp/`. If the result reports input fields and no values were supplied,
|
|
80
|
+
ask the user rather than pretending the explored surface was complete.
|
|
81
|
+
|
|
82
|
+
## Replay and gating
|
|
83
|
+
|
|
84
|
+
Flow YAML belongs under `.tapp/flows/` and can replay without a model or API key:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx -y @aarwitz/tapp@latest flow run .tapp/flows/smoke.yml
|
|
88
|
+
npx -y @aarwitz/tapp@latest ci
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`tapp explore` observes. `tapp ci` applies versioned deterministic policy to evidence, selected
|
|
92
|
+
Flows/Scenarios/contracts, coverage, and any target-scoped baseline. Its outcomes are `pass`, `fail`,
|
|
93
|
+
or `inconclusive`; both `fail` and `inconclusive` block a merge.
|
|
94
|
+
|
|
95
|
+
## Platform prerequisites
|
|
96
|
+
|
|
97
|
+
- iOS: macOS, Xcode, and a booted simulator. First use builds a cached harness under `~/.tapp`.
|
|
98
|
+
- Android: `adb` and a connected authorized emulator/device.
|
|
99
|
+
- Web: Playwright and Chromium. If Tapp reports the browser missing, run
|
|
100
|
+
`npx playwright install chromium` and retry.
|
|
101
|
+
|
|
102
|
+
Captures and screenshots are stored under `~/.tapp/captures/` and `~/.tapp/shots/`.
|