@aarwitz/tapp 0.17.8 → 0.17.9

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,7 +1,7 @@
1
1
  {
2
2
  "name": "tapp",
3
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.8",
4
+ "version": "0.17.9",
5
5
  "author": {
6
6
  "name": "Aaron Horowitz",
7
7
  "url": "https://github.com/aarwitz"
@@ -24,7 +24,7 @@
24
24
  "command": "npx",
25
25
  "args": [
26
26
  "-y",
27
- "@aarwitz/tapp@0.17.8",
27
+ "@aarwitz/tapp@0.17.9",
28
28
  "mcp"
29
29
  ],
30
30
  "cwd": "${CLAUDE_PROJECT_DIR}"
package/README.md CHANGED
@@ -349,7 +349,7 @@ jobs:
349
349
  timeout-minutes: 45
350
350
  steps:
351
351
  - uses: actions/checkout@v4
352
- - uses: aarwitz/tapp@v0.17.8 # or pin the reviewed release commit SHA
352
+ - uses: aarwitz/tapp@v0.17.9 # or pin the reviewed release commit SHA
353
353
  with:
354
354
  project: MyApp.xcodeproj # or MyApp.xcworkspace
355
355
  scheme: MyApp
@@ -399,7 +399,7 @@ Android CI runs on Linux with an emulator/device already connected. The Action c
399
399
  or accept a prebuilt one:
400
400
 
401
401
  ```yaml
402
- - uses: aarwitz/tapp@v0.17.8 # or pin the reviewed release commit SHA
402
+ - uses: aarwitz/tapp@v0.17.9 # or pin the reviewed release commit SHA
403
403
  with:
404
404
  platform: android
405
405
  android-app-id: com.acme.app
@@ -458,6 +458,15 @@ only an optional authoring/enrichment layer (`tapp_flow_generate`, `assert_ai`,
458
458
  The first tool call builds the harness once (~2 min, cached in `~/.tapp`; rebuilt automatically
459
459
  if you switch simulators). All captures land in `~/.tapp/captures/`.
460
460
 
461
+ ## Feedback
462
+
463
+ Tapp accepts feedback the way agents already work: as GitHub issues on
464
+ [aarwitz/tapp](https://github.com/aarwitz/tapp/issues). `tapp feedback "short title" --body "…"`
465
+ (or the `tapp_feedback` MCP tool) drafts an issue with the version, platform availability, and latest
466
+ capture id filled in and home paths and tokens redacted; `--submit` files it with your authenticated
467
+ `gh`. Drafts are the default because issues are public — an agent should show you the draft first.
468
+ Nothing is uploaded automatically; captures stay on your machine.
469
+
461
470
  ## License
462
471
 
463
472
  [MIT](./LICENSE)
package/bin/tapp.js CHANGED
@@ -282,6 +282,7 @@ function safeCommandUsage(verb) {
282
282
  actor: "tapp actor set NAME --email-env ENV --password-env ENV [--replace] [--project-dir DIR]\ntapp actor list [repo]",
283
283
  app: "tapp app [repo] [--no-open] [--port PORT]",
284
284
  report: "tapp report [captureId|latest]",
285
+ feedback: "tapp feedback \"short title\" [--body TEXT|--body-file FILE] [--type bug|idea|question] [--capture ID|latest|none] [--as human] [--submit] [--json]\n Drafts a public GitHub issue on aarwitz/tapp about tapp itself; --submit files it with the authenticated gh CLI.\n Exit codes: 0 drafted or filed · 1 gh submission failed · 2 usage error",
285
286
  doctor: "tapp doctor [--json]\n Exit codes: 0 environment healthy · 1 blocked (fix ❌ items)",
286
287
  install: "tapp install",
287
288
  mcp: "tapp mcp",
@@ -301,7 +302,7 @@ if (["--help", "-h"].includes(command)) {
301
302
  const knownCommands = new Set([
302
303
  "help", "version", "--version", "-v", "mcp", "init", "focus", "explore", "qa", "open",
303
304
  "tree", "shot", "screenshot", "apps", "build", "flow", "task", "contract", "scenario", "map",
304
- "pr", "plan", "baseline", "ci", "actor", "app", "studio", "report", "doctor", "install",
305
+ "pr", "plan", "baseline", "ci", "actor", "app", "studio", "report", "doctor", "install", "feedback",
305
306
  ]);
306
307
  if (!knownCommands.has(command)) {
307
308
  console.error(`❌ Unknown command: ${command}`);
@@ -695,15 +696,28 @@ switch (command) {
695
696
  const launchOptions = iosLaunchOptions(flags, rest);
696
697
  let target = positionals[0] || "";
697
698
  let baselineFindings;
699
+ let baselineCaptureContext = null;
698
700
  if (flags.baseline) {
699
701
  try {
700
702
  const parsed = JSON.parse(fs.readFileSync(flags.baseline, "utf8"));
701
703
  baselineFindings = Array.isArray(parsed) ? parsed : parsed.findings;
704
+ // Accept the 0.17.7 gate JSON's stamp-shaped `capture` too (explore JSON never
705
+ // stamped that key — there it is the evidence-folder record).
706
+ if (!Array.isArray(parsed)) baselineCaptureContext = parsed.captureContext || (parsed.capture?.viewport ? parsed.capture : null);
702
707
  } catch (e) {
703
708
  console.error(`❌ Could not read baseline ${flags.baseline}: ${e.message}`);
704
709
  process.exit(2);
705
710
  }
706
711
  }
712
+ // The same honesty rule the CI gate applies: a baseline captured at another device/viewport
713
+ // makes "resolved" a layout diff, not a fix — say so on the command the JSON hint points at.
714
+ const applyCaptureMismatch = (structured) => {
715
+ if (!structured?.regression || !baselineCaptureContext || !structured.captureContext) return;
716
+ if (JSON.stringify(baselineCaptureContext) === JSON.stringify(structured.captureContext)) return;
717
+ structured.regression.captureMismatch = { baseline: baselineCaptureContext, current: structured.captureContext };
718
+ const describe = (c) => [c?.device, c?.viewport ? `${c.viewport.width}x${c.viewport.height}` : null, c?.deviceScaleFactor ? `@${c.deviceScaleFactor}x` : null].filter(Boolean).join(" ") || "unknown";
719
+ console.error(`⚠️ Capture mismatch: the baseline was captured at ${describe(baselineCaptureContext)}, this run at ${describe(structured.captureContext)}. "Resolved" findings may reflect layout differences, not fixes — re-baseline at the same device/viewport to compare honestly.`);
720
+ };
707
721
  const engine = await engineImport();
708
722
  // Source-preparing bare explore (ADR-0005 §5): no explicit target + a repo application model →
709
723
  // drive the model's default target end to end. Managed web is built/started/waited-for and
@@ -734,6 +748,7 @@ switch (command) {
734
748
  });
735
749
  finishProgress();
736
750
  if (r.error) { printEngineError(r); process.exit(1); }
751
+ applyCaptureMismatch(r.structured);
737
752
  console.log(r.text);
738
753
  if (flags.json && typeof flags.json === "string") {
739
754
  fs.writeFileSync(flags.json, JSON.stringify(r.structured, null, 2));
@@ -804,6 +819,7 @@ switch (command) {
804
819
  printEngineError(r);
805
820
  process.exit(1);
806
821
  }
822
+ applyCaptureMismatch(r.structured);
807
823
  console.log(r.text);
808
824
  if (flags.json && typeof flags.json === "string") {
809
825
  fs.writeFileSync(flags.json, JSON.stringify(r.structured, null, 2));
@@ -1829,6 +1845,58 @@ switch (command) {
1829
1845
  break;
1830
1846
  }
1831
1847
 
1848
+ case "feedback": {
1849
+ const { flags, positionals } = parseVerbArgs(rest);
1850
+ const usage = 'Usage: tapp feedback "short title" [--body TEXT|--body-file FILE] [--type bug|idea|question] [--capture ID|latest|none] [--as human] [--submit] [--json]';
1851
+ const title = positionals.join(" ").trim();
1852
+ if (!title) { console.error(`❌ ${usage}`); process.exit(2); }
1853
+ const fb = await import(path.join(packageRoot, "mcp-server", "src", "feedback.js"));
1854
+ const type = typeof flags.type === "string" ? flags.type : "bug";
1855
+ if (!fb.FEEDBACK_TYPES.includes(type)) { console.error(`❌ --type must be one of ${fb.FEEDBACK_TYPES.join(", ")}\n${usage}`); process.exit(2); }
1856
+ let body = typeof flags.body === "string" ? flags.body : "";
1857
+ if (typeof flags["body-file"] === "string") {
1858
+ try { body = fs.readFileSync(flags["body-file"], "utf8"); } catch (error) { console.error(`❌ Could not read --body-file: ${error.message}`); process.exit(2); }
1859
+ }
1860
+ const captureFlag = typeof flags.capture === "string" ? flags.capture : "latest";
1861
+ const captureId = captureFlag === "none" ? null : captureFlag === "latest" ? fb.latestCaptureId(tappHome) : captureFlag;
1862
+ // Platform availability comes from doctor --json so the issue carries facts, not guesses.
1863
+ let doctor = null;
1864
+ try {
1865
+ const d = run(process.execPath, [path.join(packageRoot, "bin", "tapp.js"), "doctor", "--json"], { env: { ...process.env, TAPP_HOME: tappHome } });
1866
+ doctor = JSON.parse(d.stdout);
1867
+ } catch { /* feedback still works without the environment summary */ }
1868
+ let issue;
1869
+ try {
1870
+ issue = fb.composeFeedback({ title, body, type, version: pkg.version, doctor, captureId, filedBy: flags.as === "human" ? "human" : "agent" });
1871
+ } catch (error) { console.error(`❌ ${error.message}\n${usage}`); process.exit(2); }
1872
+ const url = fb.feedbackIssueUrl(issue);
1873
+ let submitted = false, issueUrl = null;
1874
+ if (flags.submit === true) {
1875
+ const status = fb.ghStatus();
1876
+ if (!status.available) {
1877
+ if (flags.json === true) console.log(JSON.stringify({ ...issue, url, submitted: false, error: "gh not authenticated" }, null, 2));
1878
+ else console.error(`❌ The GitHub CLI is not authenticated on this machine, so nothing was filed.\n Open this prefilled issue instead (it is public):\n ${url}`);
1879
+ process.exit(1);
1880
+ }
1881
+ const result = fb.submitFeedbackViaGh(issue);
1882
+ if (!result.ok) {
1883
+ if (flags.json === true) console.log(JSON.stringify({ ...issue, url, submitted: false, error: result.detail }, null, 2));
1884
+ else console.error(`❌ gh issue create failed: ${result.detail}\n Prefilled issue link: ${url}`);
1885
+ process.exit(1);
1886
+ }
1887
+ submitted = true; issueUrl = result.url;
1888
+ }
1889
+ if (flags.json === true) {
1890
+ console.log(JSON.stringify({ ...issue, url, submitted, issueUrl }, null, 2));
1891
+ break;
1892
+ }
1893
+ if (submitted) { ok("Filed feedback", issueUrl); break; }
1894
+ console.log(`📝 Feedback draft (not filed — issues on ${fb.FEEDBACK_REPO} are public; confirm with the user first):\n`);
1895
+ console.log(`Title: ${issue.title}\nLabels: ${issue.labels.join(", ")}\n\n${issue.body}`);
1896
+ console.log(`File it: tapp feedback ${JSON.stringify(issue.title)} … --submit (uses your authenticated gh)\nOr open: ${url}`);
1897
+ break;
1898
+ }
1899
+
1832
1900
  case "version":
1833
1901
  case "--version":
1834
1902
  case "-v": {
@@ -1859,6 +1927,8 @@ Repository & release:
1859
1927
  (--explore grounds the UI Map · --url URL · --platform · --dry-run · --refresh)
1860
1928
  tapp baseline create [repo] Run/import a conclusive full gate and save a target-scoped baseline
1861
1929
  tapp report [captureId] Open the HTML evidence page for a capture (default: latest)
1930
+ tapp feedback "title" Send a bug, idea, or question to the tapp maintainers as a GitHub issue
1931
+ (drafts by default · --submit files it · issues are public)
1862
1932
  tapp ci install [repo] Generate a reviewable target-aware GitHub workflow + CI manifest
1863
1933
  tapp actor set NAME Configure an actor using environment-variable names only (never values)
1864
1934
  tapp actor list [repo] Inspect named actors, sessions, provisioning, and secret env bindings
package/docs/scenarios.md CHANGED
@@ -74,7 +74,7 @@ tapp ci --platform web --url http://127.0.0.1:4180 \
74
74
  GitHub Action:
75
75
 
76
76
  ```yaml
77
- - uses: aarwitz/tapp@v0.17.8 # or pin the reviewed release commit SHA
77
+ - uses: aarwitz/tapp@v0.17.9 # or pin the reviewed release commit SHA
78
78
  with:
79
79
  platform: web
80
80
  url: http://127.0.0.1:4180
@@ -0,0 +1,97 @@
1
+ // Feedback to the tapp maintainers, the agent-native way: a GitHub issue on aarwitz/tapp.
2
+ //
3
+ // Shared by `tapp feedback` (CLI) and `tapp_feedback` (MCP). Both default to a DRAFT — the
4
+ // composed issue plus a prefilled github.com/.../issues/new URL — and only file the issue when
5
+ // explicitly asked (`--submit` / `submit: true`), because issues are public and an agent must not
6
+ // publish on a user's behalf without consent. Filing uses the machine's authenticated `gh` CLI;
7
+ // nothing is uploaded: captures stay local and only the capture id is referenced.
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ import { spawnSync } from "node:child_process";
12
+
13
+ export const FEEDBACK_REPO = "aarwitz/tapp";
14
+ export const FEEDBACK_TYPES = ["bug", "idea", "question"];
15
+ const TYPE_LABEL = { bug: "bug", idea: "idea", question: "question" };
16
+
17
+ /** Strip home directories and token-shaped secrets before anything leaves the machine. */
18
+ export function redactText(text, home = os.homedir()) {
19
+ let s = String(text ?? "");
20
+ if (home && home.length > 1) s = s.split(home).join("~");
21
+ s = s
22
+ .replace(/\/Users\/[^/\s"']+/g, "~")
23
+ .replace(/\/home\/[^/\s"']+/g, "~")
24
+ .replace(/[A-Za-z]:\\Users\\[^\\\s"']+/g, "~")
25
+ .replace(/\b(sk-ant-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16})\b/g, "[redacted]");
26
+ return s;
27
+ }
28
+
29
+ /** Newest capture directory name under TAPP_HOME, or null. Only the id is ever shared. */
30
+ export function latestCaptureId(tappHome) {
31
+ const dir = path.join(tappHome, "captures");
32
+ if (!fs.existsSync(dir)) return null;
33
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
34
+ .filter((e) => e.isDirectory())
35
+ .map((e) => ({ name: e.name, mtime: fs.statSync(path.join(dir, e.name)).mtimeMs }))
36
+ .sort((a, b) => b.mtime - a.mtime);
37
+ return entries[0]?.name ?? null;
38
+ }
39
+
40
+ /** One line of platform availability from `tapp doctor --json` output. */
41
+ export function summarizeDoctor(doctor) {
42
+ if (!doctor || typeof doctor !== "object") return null;
43
+ const p = doctor.platforms || {};
44
+ const parts = [];
45
+ if (p.ios) {
46
+ const detail = [p.ios.xcode, p.ios.bootedSimulator ? `${p.ios.bootedSimulator} booted` : ""].filter(Boolean).join(", ");
47
+ parts.push(`iOS ${p.ios.available ? "✅" : "⬜"}${detail ? ` (${detail})` : ""}`);
48
+ }
49
+ if (p.android) parts.push(`Android ${p.android.adb ? "✅" : "⬜"}${p.android.devicesConnected ? ` (${p.android.devicesConnected} device)` : ""}`);
50
+ if (p.web) parts.push(`web ${p.web.available ? "✅" : "⬜"}`);
51
+ return parts.join(" · ") || null;
52
+ }
53
+
54
+ /** Build the issue: redacted title/body plus an automatic, path-free context footer. */
55
+ export function composeFeedback({
56
+ title, body = "", type = "bug", version = "unknown", node = process.version,
57
+ platform = `${process.platform} ${process.arch}`, doctor = null, captureId = null, filedBy = "agent",
58
+ }) {
59
+ const cleanTitle = redactText(title).trim();
60
+ if (!cleanTitle) throw new Error("feedback needs a short title");
61
+ if (!FEEDBACK_TYPES.includes(type)) throw new Error(`type must be one of: ${FEEDBACK_TYPES.join(", ")}`);
62
+ const context = [`- tapp ${version} · node ${node} · ${platform}`];
63
+ const platforms = summarizeDoctor(doctor);
64
+ if (platforms) context.push(`- platforms: ${platforms}`);
65
+ if (captureId) context.push(`- capture: \`${redactText(captureId)}\` (kept locally — nothing is uploaded; evidence can be shared privately on request)`);
66
+ const description = redactText(body).trim() || "_(no description provided)_";
67
+ const byline = filedBy === "agent" ? ", filed by a coding agent on the user's behalf" : "";
68
+ const full = `${description}\n\n---\n_Filed with \`tapp feedback\` (${type}${byline}). This issue is public._\n${context.join("\n")}\n`;
69
+ const labels = ["feedback", TYPE_LABEL[type]];
70
+ if (filedBy === "agent") labels.push("agent-filed");
71
+ return { title: cleanTitle.slice(0, 200), body: full, labels };
72
+ }
73
+
74
+ /** Prefilled "new issue" link — works for anyone with a GitHub account, no CLI needed. */
75
+ export function feedbackIssueUrl({ title, body, labels }) {
76
+ const url = new URL(`https://github.com/${FEEDBACK_REPO}/issues/new`);
77
+ url.searchParams.set("title", title);
78
+ url.searchParams.set("body", body);
79
+ url.searchParams.set("labels", labels.join(","));
80
+ return url.toString();
81
+ }
82
+
83
+ export function ghStatus(ghBin = process.env.TAPP_GH_BIN || "gh") {
84
+ const r = spawnSync(ghBin, ["auth", "status"], { encoding: "utf8" });
85
+ return { available: r.status === 0, detail: (r.stderr || r.stdout || "").trim() };
86
+ }
87
+
88
+ export function submitFeedbackViaGh(issue, ghBin = process.env.TAPP_GH_BIN || "gh") {
89
+ const r = spawnSync(ghBin, [
90
+ "issue", "create", "--repo", FEEDBACK_REPO,
91
+ "--title", issue.title, "--body-file", "-", "--label", issue.labels.join(","),
92
+ ], { encoding: "utf8", input: issue.body });
93
+ const out = (r.stdout || "").trim();
94
+ const err = (r.stderr || "").trim();
95
+ const url = (out.match(/https:\/\/github\.com\/\S+/) || [])[0] || null;
96
+ return { ok: r.status === 0 && Boolean(url), url, detail: r.status === 0 ? out : (err || out) };
97
+ }
@@ -2438,6 +2438,27 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
2438
2438
 
2439
2439
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
2440
2440
  tools: [
2441
+ {
2442
+ name: "tapp_feedback",
2443
+ title: "Send feedback to the tapp maintainers",
2444
+ description:
2445
+ "Draft a public GitHub issue on aarwitz/tapp about tapp itself — a bug in tapp, an idea, or a " +
2446
+ "question (not a finding about the app under test). Adds the tapp version, platform availability, " +
2447
+ "and the latest capture id automatically and redacts home paths and tokens. Default is a DRAFT: " +
2448
+ "returns the composed issue plus a prefilled github.com URL the user can open. Pass submit:true only " +
2449
+ "after the user has agreed to file a public issue; filing uses this machine's authenticated GitHub CLI.",
2450
+ inputSchema: {
2451
+ type: "object",
2452
+ properties: {
2453
+ title: { type: "string", description: "Short, specific title" },
2454
+ body: { type: "string", description: "What happened, what you expected, the exact command or tool call. No secrets or private source." },
2455
+ type: { type: "string", enum: ["bug", "idea", "question"], description: "Default bug" },
2456
+ capture: { type: "string", description: "Capture id to reference, 'latest' (default) or 'none'" },
2457
+ submit: { type: "boolean", description: "File the issue now with gh (needs the user's consent). Default false = draft only." },
2458
+ },
2459
+ required: ["title"],
2460
+ },
2461
+ },
2441
2462
  {
2442
2463
  name: "tapp_health",
2443
2464
  title: "Check Tapp readiness",
@@ -3170,6 +3191,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3170
3191
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
3171
3192
  const { name, arguments: args = {} } = request.params;
3172
3193
 
3194
+ if (name === "tapp_feedback") {
3195
+ const fb = await import("./feedback.js");
3196
+ const { fileURLToPath } = await import("node:url");
3197
+ const packageRootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
3198
+ const title = String(args.title || "").trim();
3199
+ if (!title) return errorResult("tapp_feedback needs a short title", {});
3200
+ const type = typeof args.type === "string" ? args.type : "bug";
3201
+ if (!fb.FEEDBACK_TYPES.includes(type)) return errorResult(`type must be one of ${fb.FEEDBACK_TYPES.join(", ")}`, {});
3202
+ const tappHomeDir = (process.env.TAPP_HOME || path.join(os.homedir(), ".tapp")).trim();
3203
+ const captureArg = typeof args.capture === "string" ? args.capture : "latest";
3204
+ const captureId = captureArg === "none" ? null : captureArg === "latest" ? fb.latestCaptureId(tappHomeDir) : captureArg;
3205
+ let doctor = null;
3206
+ try {
3207
+ const d = await runCommand(process.execPath, [path.join(packageRootDir, "bin", "tapp.js"), "doctor", "--json"], { timeoutMs: 60_000, env: { TAPP_HOME: tappHomeDir } });
3208
+ doctor = JSON.parse(d.stdout);
3209
+ } catch { /* optional context */ }
3210
+ const version = JSON.parse(fs.readFileSync(path.join(packageRootDir, "package.json"), "utf8")).version;
3211
+ let issue;
3212
+ try { issue = fb.composeFeedback({ title, body: String(args.body || ""), type, version, doctor, captureId, filedBy: "agent" }); }
3213
+ catch (error) { return errorResult(error.message, {}); }
3214
+ const url = fb.feedbackIssueUrl(issue);
3215
+ if (args.submit === true) {
3216
+ const status = fb.ghStatus();
3217
+ if (!status.available) return errorResult("The GitHub CLI is not authenticated on this machine, so nothing was filed. Give the user the prefilled link instead.", { url, ...issue, submitted: false });
3218
+ const result = fb.submitFeedbackViaGh(issue);
3219
+ if (!result.ok) return errorResult(`gh issue create failed: ${result.detail}`, { url, ...issue, submitted: false });
3220
+ return richResult(`✅ Filed feedback: ${result.url}`, { submitted: true, issueUrl: result.url, ...issue });
3221
+ }
3222
+ return richResult(
3223
+ `📝 Feedback drafted, not filed. Issues on ${fb.FEEDBACK_REPO} are public — confirm with the user, then call tapp_feedback again with submit: true, or give them this prefilled link:\n${url}\n\nTitle: ${issue.title}\nLabels: ${issue.labels.join(", ")}\n\n${issue.body}`,
3224
+ { submitted: false, url, ...issue },
3225
+ );
3226
+ }
3227
+
3173
3228
  if (name === "tapp_health") {
3174
3229
  const coreChecks = [];
3175
3230
  coreChecks.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.17.8",
3
+ "version": "0.17.9",
4
4
  "mcpName": "io.github.aarwitz/tapp",
5
5
  "description": "Let coding agents verify UI changes on real iOS, Android, and web surfaces, then enforce reviewed proof in deterministic CI.",
6
6
  "license": "MIT",
@@ -183,8 +183,8 @@ run_harness_test() {
183
183
  "OCQA_BUNDLE_ID": "$bundle_id",
184
184
  "OCQA_MAX_ACTIONS": "$max_actions",
185
185
  "OCQA_TIMEOUT_SECONDS": "$timeout_secs",
186
- "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
187
- "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$interactive_line$overrides_line$launch_args_line$launch_env_line$login_steps_line$pr_target_line$visual_ready_line$recording_started_line
186
+ "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-}",
187
+ "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-}"$interactive_line$overrides_line$launch_args_line$launch_env_line$login_steps_line$pr_target_line$visual_ready_line$recording_started_line
188
188
  }
189
189
  CONF
190
190
 
@@ -447,8 +447,8 @@ OCQA_COMPLETE:{\"actions\":0,\"states\":0,\"issues\":1,\"screens\":\"\",\"outcom
447
447
  "OCQA_SESSION_CMD_PATH": "$SESS_CMD",
448
448
  "OCQA_SESSION_RESULT_PATH": "$SESS_RES",
449
449
  "OCQA_SESSION_TIMEOUT": "$SESS_TIMEOUT",
450
- "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-qa@example.com}",
451
- "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-Tapp123!}"$sess_args_line$sess_env_line
450
+ "OCQA_TEST_EMAIL": "${OCQA_TEST_EMAIL:-}",
451
+ "OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-}"$sess_args_line$sess_env_line
452
452
  }
453
453
  CONF
454
454
  xctestrun=$(find "$HARNESS_DERIVED/Build/Products" -name "*.xctestrun" 2>/dev/null | head -1)
@@ -92,7 +92,7 @@ After the focused fast path, read returned `elements[]` before any remaining act
92
92
  ids or visible labels, check `hittable`, tap a field before typing, and wait for navigation or async
93
93
  content. Use coordinates only as a last resort. End the session when finished.
94
94
 
95
- Do not edit the app merely because testing found a defect unless the user also asked for a fix. State
96
- what the evidence proves and what remains untested.
95
+ Do not edit the app merely because testing found a defect unless the user also asked for a fix. State what the evidence proves and what remains untested.
96
+ If tapp itself misbehaves, draft feedback with `npx -y @aarwitz/tapp@latest feedback "title"` or `tapp_feedback`; it is public — draft first, `--submit` only with the user's OK.
97
97
 
98
98
  Read [references/commands.md](references/commands.md) only for exact CLI/MCP syntax, Flow replay, credentials, or platform prerequisites.
@@ -14,6 +14,7 @@ npx -y @aarwitz/tapp@latest tree [target] --json
14
14
  npx -y @aarwitz/tapp@latest shot
15
15
  npx -y @aarwitz/tapp@latest report latest
16
16
  npx -y @aarwitz/tapp@latest doctor
17
+ npx -y @aarwitz/tapp@latest feedback "short title" --body "what happened" --type bug|idea|question # draft; add --submit to file (public)
17
18
  ```
18
19
 
19
20
  Platform examples: