@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.
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +11 -2
- package/bin/tapp.js +71 -1
- package/docs/scenarios.md +1 -1
- package/mcp-server/src/feedback.js +97 -0
- package/mcp-server/src/index.js +55 -0
- package/package.json +1 -1
- package/scripts/quick-capture.sh +4 -4
- package/skills/tapp/SKILL.md +2 -2
- package/skills/tapp/references/commands.md +1 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/mcp-server/src/index.js
CHANGED
|
@@ -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.
|
|
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",
|
package/scripts/quick-capture.sh
CHANGED
|
@@ -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:-
|
|
187
|
-
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-
|
|
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:-
|
|
451
|
-
"OCQA_TEST_PASSWORD": "${OCQA_TEST_PASSWORD:-
|
|
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)
|
package/skills/tapp/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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:
|