@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/AGENTS.md +26 -21
- package/Harness/OCQAHarnessUITests/ExplorerTests.swift +62 -22
- package/README.md +64 -67
- package/bin/tapp.js +168 -45
- package/browser/app.js +12 -6
- package/docs/BROWSER-PRODUCT.md +75 -0
- package/docs/PRODUCT-ENGINE.md +107 -0
- package/docs/application-model.md +271 -0
- package/docs/scenarios.md +95 -0
- package/mcp-server/src/android-driver.js +15 -2
- package/mcp-server/src/android-explorer.js +54 -12
- package/mcp-server/src/application-model.js +9 -8
- package/mcp-server/src/browser-product.js +1 -1
- package/mcp-server/src/ci-report.js +84 -62
- package/mcp-server/src/ci-setup.js +35 -5
- package/mcp-server/src/enrich.js +1 -1
- package/mcp-server/src/html-report.js +20 -6
- package/mcp-server/src/index.js +182 -67
- package/mcp-server/src/pr-selection.js +4 -3
- package/mcp-server/src/product-execution.js +1 -1
- package/mcp-server/src/product-operations.js +2 -2
- package/mcp-server/src/project-config.js +1 -2
- package/mcp-server/src/project-paths.js +5 -17
- package/mcp-server/src/release-contract.js +3 -3
- package/mcp-server/src/report.js +185 -51
- package/mcp-server/src/task-runtime.js +1 -1
- package/mcp-server/src/web-explorer.js +1 -1
- package/package.json +2 -2
- package/scripts/ci-gate.sh +9 -8
- package/scripts/platform-gate.js +11 -5
- package/scripts/quick-capture.sh +72 -38
- package/scripts/run-flow.sh +1 -1
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
|
|
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 ||
|
|
33
|
+
const tappHome = (process.env.TAPP_HOME || path.join(os.homedir(), ".tapp")).trim();
|
|
34
34
|
process.env.TAPP_HOME = tappHome;
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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"}
|
|
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.
|
|
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
|
|
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
|
|
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} ${
|
|
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
|
|
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
|
-
|
|
569
|
-
|
|
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
|
|
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
|
|
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
|
|
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 ·
|
|
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
|
|
1487
|
+
console.log(`tapp v${pkg.version} — ship with proof. Autonomous exploration and deterministic release gates for iOS, Android, and web.
|
|
1376
1488
|
|
|
1377
|
-
|
|
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
|
-
|
|
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 (
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|