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

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": "Test real iOS, Android, and web apps with Tapp. Includes the Tapp agent skill plus MCP hands, eyes, exploration, replayable Flows, and deterministic release evidence.",
4
- "version": "0.17.0-rc.13",
4
+ "version": "0.17.0-rc.14",
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.0-rc.13",
27
+ "@aarwitz/tapp@0.17.0-rc.14",
28
28
  "mcp"
29
29
  ],
30
30
  "cwd": "${CLAUDE_PROJECT_DIR}"
package/bin/tapp.js CHANGED
@@ -155,7 +155,7 @@ const engineImport = () => import(path.join(packageRoot, "mcp-server", "src", "i
155
155
 
156
156
  function requireMacFor(what) {
157
157
  if (process.platform === "darwin") return;
158
- console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: tapp explore https://localhost:3000`);
158
+ console.error(`❌ ${what} requires macOS (Xcode + iOS simulator). The web beta runs anywhere: npx -y @aarwitz/tapp@latest explore https://localhost:3000`);
159
159
  process.exit(1);
160
160
  }
161
161
 
@@ -840,7 +840,7 @@ switch (command) {
840
840
  process.exit(1);
841
841
  }
842
842
  console.log(`🔨 Built ${path.basename(built.appPath)} (scheme ${built.scheme}) — installed as ${inst.bundleId}`);
843
- console.log(`\nNext: tapp explore ${inst.bundleId}`);
843
+ console.log(`\nNext: npx -y @aarwitz/tapp@latest explore ${inst.bundleId}`);
844
844
  break;
845
845
  }
846
846
 
@@ -1054,7 +1054,7 @@ switch (command) {
1054
1054
  try {
1055
1055
  const adopted = adoptPrCoverageProposal({ projectDir, prPlanPath, item: flags.item, releasePlanPath: typeof flags["release-plan"] === "string" ? flags["release-plan"] : undefined });
1056
1056
  console.log(`📥 ${adopted.mode === "reconciled-existing" ? "Reconciled PR evidence into" : "Adopted"} ${adopted.item.name}${adopted.mode === "reconciled-existing" ? ` while preserving decision '${adopted.item.decision}'` : " as a pending release-plan item"}; no Task or contract was generated or trusted`);
1057
- console.log(` plan: ${adopted.path}\n next: tapp plan review ${adopted.path} --approve ${adopted.item.id}`);
1057
+ console.log(` plan: ${adopted.path}\n next: npx -y @aarwitz/tapp@latest plan review ${adopted.path} --approve ${adopted.item.id}`);
1058
1058
  } catch (error) { console.error(`❌ Could not adopt PR coverage proposal: ${error.message || String(error)}`); process.exit(2); }
1059
1059
  break;
1060
1060
  }
@@ -1223,7 +1223,7 @@ switch (command) {
1223
1223
  const booted = bootedSims();
1224
1224
  ok("iOS", `${ver || "Xcode"}; ${booted.length ? `${booted[0].name} booted` : "no simulator booted yet"}`);
1225
1225
  const xctestrun = harnessXctestrun();
1226
- xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: tapp install)");
1226
+ xctestrun ? ok("iOS harness cache", xctestrun) : console.log(" ⬜ iOS harness cache — builds on first use (or: npx -y @aarwitz/tapp@latest install)");
1227
1227
  } else {
1228
1228
  console.log(" ⬜ iOS — unavailable (install Xcode + simulator runtime)");
1229
1229
  }
@@ -1257,7 +1257,7 @@ switch (command) {
1257
1257
  console.log(`\n Home: ${tappHome}`);
1258
1258
  console.log(healthy
1259
1259
  ? "\nReady. Start with:\n npx -y @aarwitz/tapp@latest open [target]\n npx -y @aarwitz/tapp@latest explore [target]"
1260
- : "\nFix the ❌ items above, then re-run: tapp doctor");
1260
+ : "\nFix the ❌ items above, then re-run: npx -y @aarwitz/tapp@latest doctor");
1261
1261
  process.exit(healthy ? 0 : 1);
1262
1262
  }
1263
1263
 
@@ -1332,7 +1332,7 @@ switch (command) {
1332
1332
  console.log(`✅ Actor '${name}' configured — ${result.actor.session} session · ${result.actor.provisioning} provisioning`);
1333
1333
  console.log(` ${result.path}`);
1334
1334
  console.log(` bindings: ${Object.entries(result.actor.credentials).map(([key, binding]) => `${key}=$${binding.env}`).join(", ") || "none"}`);
1335
- console.log(" No credential values were accepted or written. Rerun tapp init --refresh to update the application model.");
1335
+ console.log(" No credential values were accepted or written. Rerun npx -y @aarwitz/tapp@latest init --refresh to update the application model.");
1336
1336
  } catch (error) { console.error(`❌ Actor not configured: ${error.message || String(error)}`); process.exit(2); }
1337
1337
  break;
1338
1338
  }
@@ -1347,7 +1347,7 @@ switch (command) {
1347
1347
  const projectDir = fs.realpathSync(path.resolve(positionals[1] || (typeof flags["project-dir"] === "string" ? flags["project-dir"] : process.cwd())));
1348
1348
  const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
1349
1349
  if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
1350
- console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore, review/generate/validate/promote the plan, then create the baseline.`);
1350
+ console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore, review/generate/validate/promote the plan, then create the baseline.`);
1351
1351
  process.exit(2);
1352
1352
  }
1353
1353
  let model;
@@ -1454,7 +1454,7 @@ switch (command) {
1454
1454
  catch { console.error(`❌ Repository directory not found: ${positionals[0] || flags["project-dir"] || process.cwd()}`); process.exit(2); }
1455
1455
  const modelPath = typeof flags.model === "string" ? path.resolve(projectDir, flags.model) : existingProjectArtifactPath(projectDir, "application-model.json");
1456
1456
  if (!modelPath.startsWith(projectDir + path.sep) || !fs.existsSync(modelPath)) {
1457
- console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run tapp init --explore first.`);
1457
+ console.error(`❌ Application model not found inside the repository: ${modelPath}\n Run npx -y @aarwitz/tapp@latest init --explore first.`);
1458
1458
  process.exit(2);
1459
1459
  }
1460
1460
  const actionRef = typeof flags["action-ref"] === "string" ? flags["action-ref"] : `aarwitz/tapp@v${pkg.version}`;
@@ -1575,7 +1575,7 @@ switch (command) {
1575
1575
 
1576
1576
  Core — explore, prove, gate (agents and humans can just run these — no server, no setup):
1577
1577
  tapp explore [target] Autonomous exploration → findings + evidence (an observation, NOT a
1578
- release decision — run 'tapp ci' to gate a merge)
1578
+ release decision — run 'npx -y @aarwitz/tapp@latest ci' to gate a merge)
1579
1579
  (web: --watch · all: --platform ios|android|web · --actions N)
1580
1580
  tapp contract run FILE Replay a business-level release contract — the guarantees that must hold
1581
1581
  tapp ci ... Merge-blocking release gate — explore + suites + baseline → pass/fail/inconclusive
@@ -541,10 +541,10 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
541
541
  const requirements = [];
542
542
  if (!targets.length) requirements.push({ id: "target", severity: "blocking", status: "missing", message: "No iOS application project, Android application module, or browser application target was detected.", remediation: "Pass the representative target explicitly or add its unavoidable build/runtime configuration." });
543
543
  for (const target of targets) {
544
- if (target.platform === "ios" && target.status !== "configured") requirements.push({ id: `${target.id}:scheme`, severity: "blocking", status: "needs-confirmation", message: `Confirm a shared build scheme for ${target.name}.`, remediation: `Run tapp build ${target.sourcePath} or provide the scheme during init/build.` });
544
+ if (target.platform === "ios" && target.status !== "configured") requirements.push({ id: `${target.id}:scheme`, severity: "blocking", status: "needs-confirmation", message: `Confirm a shared build scheme for ${target.name}.`, remediation: `Run npx -y @aarwitz/tapp@latest build ${target.sourcePath} or provide the scheme during init/build.` });
545
545
  if (target.platform === "android" && !target.runtime.applicationId) requirements.push({ id: `${target.id}:application-id`, severity: "blocking", status: "missing", message: `Android application id was not statically detected for ${target.name}.`, remediation: "Provide --app-id or expose applicationId in the application module." });
546
- if (target.platform === "web" && !target.runtime.ownedUrl && target.runtime.management !== "tapp-managed") requirements.push({ id: `${target.id}:owned-url`, severity: "blocking", status: "missing", message: `No safe managed runtime or owned URL is available for ${target.name}.`, remediation: "Add a deterministic start/static target or start the app and rerun tapp init with --url http://127.0.0.1:<port>." });
547
- if (target.platform === "web" && target.build.dependencyStatus === "missing-lockfile") requirements.push({ id: `${target.id}:dependency-lock`, severity: "blocking", status: "missing", message: `${target.name} declares browser dependencies without an observed dependency lockfile.`, remediation: "Commit the package-manager lockfile so Tapp can install dependencies reproducibly, then rerun tapp init." });
546
+ if (target.platform === "web" && !target.runtime.ownedUrl && target.runtime.management !== "tapp-managed") requirements.push({ id: `${target.id}:owned-url`, severity: "blocking", status: "missing", message: `No safe managed runtime or owned URL is available for ${target.name}.`, remediation: "Add a deterministic start/static target or start the app and rerun npx -y @aarwitz/tapp@latest init with --url http://127.0.0.1:<port>." });
547
+ if (target.platform === "web" && target.build.dependencyStatus === "missing-lockfile") requirements.push({ id: `${target.id}:dependency-lock`, severity: "blocking", status: "missing", message: `${target.name} declares browser dependencies without an observed dependency lockfile.`, remediation: "Commit the package-manager lockfile so Tapp can install dependencies reproducibly, then rerun npx -y @aarwitz/tapp@latest init." });
548
548
  }
549
549
  const incompleteMaps = uiMaps.filter((record) => record.summary.status !== "observed");
550
550
  for (const record of incompleteMaps.length ? incompleteMaps : (!targets.length && uiMap.status !== "observed" ? [{ summary: uiMap }] : [])) {
@@ -553,7 +553,7 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
553
553
  requirements.push({
554
554
  id: targets.length <= 1 ? "ui-map" : `${target.id}:ui-map`, severity: "blocking", status: summary.status === "inconclusive" ? "inconclusive" : "missing",
555
555
  message: target ? (summary.status === "inconclusive" ? `The UI Map for ${target.name} is inconclusive.` : `No grounded UI Map exists for ${target.name}.`) : "No repository UI Map has been grounded in a real run.",
556
- remediation: target ? `Build/launch ${target.name}, explore the real target, and retain its map at ${summary.expectedPath || summary.path}.` : "Build/launch the target and run tapp init --explore so real exploration evidence is merged into .tapp/ui-map.json.",
556
+ remediation: target ? `Build/launch ${target.name}, explore the real target, and retain its map at ${summary.expectedPath || summary.path}.` : "Build/launch the target and run npx -y @aarwitz/tapp@latest init --explore so real exploration evidence is merged into .tapp/ui-map.json.",
557
557
  });
558
558
  }
559
559
  if (!contracts.length) requirements.push({ id: "contracts", severity: "warning", status: "missing", message: "No reviewed release contracts exist yet.", remediation: "Review the proposed release plan, then generate and validate a compact set of contracts." });
@@ -561,7 +561,7 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
561
561
  for (const error of projectConfiguration.errors) requirements.push({ id: stableId("project-config-error", error), severity: "blocking", status: "invalid", message: error, remediation: `Fix ${projectConfiguration.relativePath}; actor configuration must contain environment-variable bindings, never credential values.` });
562
562
  for (const actor of actors) {
563
563
  const missingBindings = actor.credentialRequirements.filter((credential) => !actor.credentialBindings[credential]);
564
- if (missingBindings.length) requirements.push({ id: `actor:${actor.name}:credential-bindings`, severity: "blocking", status: "missing", message: `Actor '${actor.name}' has unbound credential requirements: ${missingBindings.join(", ")}.`, remediation: `Run tapp actor set ${actor.name} --credential <name>=<ENV_NAME> for each credential, then replace any literal contract credentials with $ENV_NAME placeholders.` });
564
+ if (missingBindings.length) requirements.push({ id: `actor:${actor.name}:credential-bindings`, severity: "blocking", status: "missing", message: `Actor '${actor.name}' has unbound credential requirements: ${missingBindings.join(", ")}.`, remediation: `Run npx -y @aarwitz/tapp@latest actor set ${actor.name} --credential <name>=<ENV_NAME> for each credential, then replace any literal contract credentials with $ENV_NAME placeholders.` });
565
565
  if (actor.bindingConflicts.length) requirements.push({ id: `actor:${actor.name}:credential-conflicts`, severity: "blocking", status: "conflict", message: `Actor '${actor.name}' has conflicting credential environment bindings.`, remediation: `Align ${projectConfiguration.relativePath} and reviewed contracts; Tapp will not guess which secret binding is correct.` });
566
566
  }
567
567
  for (const error of taskErrors) requirements.push({ id: stableId("task-error", error.path), severity: "blocking", status: "invalid", message: error.error, remediation: `Fix ${error.path} before generation.` });
@@ -997,7 +997,7 @@ function edgeSupportsPlatform(edge, nodes, platform) {
997
997
  function shortestObservedPath(map, platform, targetId, { startNodeId } = {}) {
998
998
  const nodes = new Map((map.nodes || []).map((node) => [node.id, node]));
999
999
  const entryId = startNodeId || map.app?.navigationRoots?.[platform] || map.app?.entryNodes?.[platform];
1000
- if (!entryId || !nodes.has(entryId)) throw new Error(`UI Map has no observed ${platform} navigation root; rerun tapp init --refresh --explore before generating Tasks`);
1000
+ if (!entryId || !nodes.has(entryId)) throw new Error(`UI Map has no observed ${platform} navigation root; rerun npx -y @aarwitz/tapp@latest init --refresh --explore before generating Tasks`);
1001
1001
  if (!nodes.has(targetId)) throw new Error(`UI Map proposal target '${targetId}' is no longer present`);
1002
1002
  if (entryId === targetId) return [];
1003
1003
  const outgoing = new Map();
@@ -422,10 +422,10 @@ function renderMarkdown(report, regression, flows, scenarios, contracts, prPlan,
422
422
  ? ` at \`${target.navigation.route}\``
423
423
  : target.navigation?.mode === "ui-map-path" ? ` through ${(target.navigation.steps || []).length} observed map edge(s)` : "";
424
424
  lines.push(`- ${icon} PR exploration **${target.node?.name || target.id}** — ${target.execution?.status || target.status}${navigation}`);
425
- if (target.coverageProposal?.status === "awaiting-explicit-adoption") lines.push(` - Reviewable coverage proposal ready; never auto-applied. Next: \`tapp pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
425
+ if (target.coverageProposal?.status === "awaiting-explicit-adoption") lines.push(` - Reviewable coverage proposal ready; never auto-applied. Next: \`npx -y @aarwitz/tapp@latest pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
426
426
  if (target.existingReleasePlanItem) {
427
427
  lines.push(` - Existing release-plan item \`${target.existingReleasePlanItem.name}\` remains ${target.existingReleasePlanItem.decision}; no duplicate or decision change was made.`);
428
- lines.push(` - Optional explicit evidence reconciliation: \`tapp pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
428
+ lines.push(` - Optional explicit evidence reconciliation: \`npx -y @aarwitz/tapp@latest pr adopt <pr-plan.json> --item ${target.id} --project-dir .\``);
429
429
  }
430
430
  }
431
431
  for (const candidate of prPlan.maintenanceCandidates || []) {
@@ -17,7 +17,7 @@ export function targetSlug(value) {
17
17
 
18
18
  export function selectApplicationTarget(model, { platform = "", target = "", useDefault = false } = {}) {
19
19
  if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets)) {
20
- throw new Error("Expected a Tapp application model; run tapp init first");
20
+ throw new Error("Expected a Tapp application model; run npx -y @aarwitz/tapp@latest init first");
21
21
  }
22
22
  const selectedPlatform = String(platform || "").toLowerCase();
23
23
  let candidates = model.targets.filter((item) => !selectedPlatform || item.platform === selectedPlatform);
@@ -215,7 +215,7 @@ function credentialConfiguration(model, targetContracts = []) {
215
215
  if (requirements.has("email")) bindings.add("TAPP_TEST_EMAIL");
216
216
  if (requirements.has("password")) bindings.add("TAPP_TEST_PASSWORD");
217
217
  }
218
- for (const name of bindings) if (!/^[A-Z_][A-Z0-9_]{0,127}$/.test(String(name))) throw new Error(`Actor credential binding '${name}' is not a safe environment-variable name; rerun tapp init from valid .tapp/project.json`);
218
+ for (const name of bindings) if (!/^[A-Z_][A-Z0-9_]{0,127}$/.test(String(name))) throw new Error(`Actor credential binding '${name}' is not a safe environment-variable name; rerun npx -y @aarwitz/tapp@latest init from valid .tapp/project.json`);
219
219
  const inputs = {};
220
220
  const primaryEmail = primary.credentialBindings?.email || (!primary.credentialBindings && requirements.has("email") ? "TAPP_TEST_EMAIL" : "");
221
221
  const primaryPassword = primary.credentialBindings?.password || (!primary.credentialBindings && requirements.has("password") ? "TAPP_TEST_PASSWORD" : "");
@@ -241,7 +241,7 @@ function targetInputs(root, model, target) {
241
241
  const androidProjectPath = path.resolve(root, androidProject);
242
242
  const androidProjectRelative = path.relative(root, androidProjectPath);
243
243
  if (path.isAbsolute(androidProjectRelative) || androidProjectRelative === ".." || androidProjectRelative.startsWith(`..${path.sep}`)) unresolved.push("Android Gradle project resolves outside the repository");
244
- else if (!fs.existsSync(path.join(androidProjectPath, "gradlew"))) unresolved.push(`Gradle wrapper is missing from ${androidProject}; commit gradlew or rerun tapp init from the Gradle repository root`);
244
+ else if (!fs.existsSync(path.join(androidProjectPath, "gradlew"))) unresolved.push(`Gradle wrapper is missing from ${androidProject}; commit gradlew or rerun npx -y @aarwitz/tapp@latest init from the Gradle repository root`);
245
245
  inputs["android-project"] = androidProject;
246
246
  inputs["android-task"] = target.build?.task || "assembleDebug";
247
247
  if (target.runtime?.applicationId) inputs["android-app-id"] = target.runtime.applicationId;
@@ -282,7 +282,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
282
282
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
283
283
  if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+$/.test(String(actionRef || ""))) throw new Error("--action-ref must be owner/repository@release-tag-or-sha");
284
284
  if (!/^[A-Za-z0-9._\/-]+$/.test(defaultBranch) || defaultBranch.startsWith("/")) throw new Error("--default-branch contains unsupported characters");
285
- if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets) || !model.targets.length) throw new Error("Application model has no targets; run tapp init first");
285
+ if (model?.kind !== "tapp-application-model" || !Array.isArray(model.targets) || !model.targets.length) throw new Error("Application model has no targets; run npx -y @aarwitz/tapp@latest init first");
286
286
  const occupied = new Set();
287
287
  const jobs = [];
288
288
  const manifestTargets = [];
@@ -1335,9 +1335,9 @@ function fmtDuration(ms) {
1335
1335
  export function qaNextSteps(report, surface = "mcp") {
1336
1336
  if (surface === "cli") {
1337
1337
  const next = [];
1338
- if (report?.findings?.length) next.push("inspect the evidence with `tapp report latest`");
1339
- next.push("re-run with `--baseline <report.json>` to compare a fix (then `tapp ci` to gate it)");
1340
- next.push("replay a committed journey with `tapp flow run <file>`");
1338
+ if (report?.findings?.length) next.push("inspect the evidence with `npx -y @aarwitz/tapp@latest report latest`");
1339
+ next.push("re-run with `--baseline <report.json>` to compare a fix (then `npx -y @aarwitz/tapp@latest ci` to gate it)");
1340
+ next.push("replay a committed journey with `npx -y @aarwitz/tapp@latest flow run <file>`");
1341
1341
  return next;
1342
1342
  }
1343
1343
  const next = [];
@@ -1390,7 +1390,7 @@ function formatQaReport(report, { regression, inputHint, timedOut, bundleId, aiC
1390
1390
  // The merge decision is the gate's job (tapp ci), not exploration's (ADR-0005).
1391
1391
  L.push("");
1392
1392
  L.push(
1393
- `**Since last run** — +${regression.counts.new} new · ${regression.counts.persisting} persisting · ${regression.counts.resolved} resolved (comparison only — run \`tapp ci\` to gate)`
1393
+ `**Since last run** — +${regression.counts.new} new · ${regression.counts.persisting} persisting · ${regression.counts.resolved} resolved (comparison only — run \`npx -y @aarwitz/tapp@latest ci\` to gate)`
1394
1394
  );
1395
1395
  }
1396
1396
  if (inputHint) {
@@ -1801,7 +1801,7 @@ export async function runExploreTarget({
1801
1801
 
1802
1802
  const modelPath = existingProjectArtifactPath(root, "application-model.json");
1803
1803
  if (!fs.existsSync(modelPath)) {
1804
- return { error: "No application model found — run `tapp init` first, or pass an explicit target (a bundle id, a path/to/App.app, a repo dir, --app-id/--apk, or an http(s) URL)." };
1804
+ return { error: "No application model found — call `tapp_init` or run `npx -y @aarwitz/tapp@latest init` first, or pass an explicit target (a bundle id, a path/to/App.app, a repo dir, --app-id/--apk, or an http(s) URL)." };
1805
1805
  }
1806
1806
  let model;
1807
1807
  try { model = JSON.parse(fs.readFileSync(modelPath, "utf8")); }
@@ -1839,7 +1839,7 @@ export async function runExploreTarget({
1839
1839
 
1840
1840
  if (selectedPlatform === "android") {
1841
1841
  const appId = String(selected.runtime?.applicationId || "").trim();
1842
- if (!appId) return { error: `The Android target '${selected.name}' has no confirmed application id — confirm it and rerun \`tapp init\`, or pass --app-id.` };
1842
+ if (!appId) return { error: `The Android target '${selected.name}' has no confirmed application id — confirm it and call \`tapp_init\` or rerun \`npx -y @aarwitz/tapp@latest init\`, or pass --app-id.` };
1843
1843
  const task = selected.build?.task || "assembleDebug";
1844
1844
  onStatus(`Building the Android APK (${task})…`);
1845
1845
  const built = await buildAndroidApp({
@@ -2322,7 +2322,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2322
2322
  "Findings from a previous run (pass back the `findings` array a prior tapp_explore returned). " +
2323
2323
  "When provided, the result adds a `regression` COMPARISON {counts:{new,persisting,resolved}, " +
2324
2324
  "newFindings, resolved} vs. that baseline — an observation, not a gate signal. To gate a merge, " +
2325
- "run `tapp ci` (or the GitHub Action): it applies the deterministic policy and returns the " +
2325
+ "run `npx -y @aarwitz/tapp@latest ci` (or the GitHub Action): it applies the deterministic policy and returns the " +
2326
2326
  "pass/fail/inconclusive outcome.",
2327
2327
  },
2328
2328
  },
@@ -2858,7 +2858,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2858
2858
  const icon = c.ok ? "✅" : c.required ? "❌" : "⚪️";
2859
2859
  L.push(`- ${icon} **${c.check}** — ${String(c.value).split("\n")[0] || "—"}`);
2860
2860
  }
2861
- if (!ready) L.push("", "Run `tapp doctor` in the application repository for exact remediation.");
2861
+ if (!ready) L.push("", "Run `npx -y @aarwitz/tapp@latest doctor` in the application repository for exact remediation.");
2862
2862
  return richResult(L.join("\n"), { ok: ready, workspaceRoot, checks, platforms: Object.fromEntries(platformChecks.map((check) => [check.check.toLowerCase(), check.ok])) });
2863
2863
  }
2864
2864
 
@@ -410,7 +410,7 @@ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releaseP
410
410
  if (!prPlanPath || !fs.existsSync(source)) throw new Error(`PR plan not found: ${source || "(missing path)"}`);
411
411
  const targetPath = releasePlanPath === ".tapp/release-plan.json" ? existingProjectArtifactPath(root, "release-plan.json") : path.resolve(root, releasePlanPath);
412
412
  if (!inside(root, targetPath)) throw new Error("Release plan path must stay inside the project directory");
413
- if (!fs.existsSync(targetPath)) throw new Error(`Release plan not found: ${targetPath}; run tapp init first`);
413
+ if (!fs.existsSync(targetPath)) throw new Error(`Release plan not found: ${targetPath}; run npx -y @aarwitz/tapp@latest init first`);
414
414
  const prPlan = JSON.parse(fs.readFileSync(source, "utf8"));
415
415
  if (prPlan?.schemaVersion !== 1 || !Array.isArray(prPlan.explorationTargets)) throw new Error("PR plan must be an executed Tapp PR plan v1");
416
416
  const matches = prPlan.explorationTargets.filter((target) => target.id === item);
@@ -428,7 +428,7 @@ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releaseP
428
428
  if (!ground || !fs.existsSync(mapPath)) throw new Error("Coverage proposal requires the repository's persistent UI Map");
429
429
  const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
430
430
  const node = (map.nodes || []).find((candidate) => candidate.id === ground.id && candidate.status !== "proposed");
431
- const refreshAdvice = "refresh the persistent UI Map with `tapp init --explore --refresh`, rerun `tapp pr gate`, then retry `tapp pr adopt`";
431
+ const refreshAdvice = "refresh the persistent UI Map with `npx -y @aarwitz/tapp@latest init --explore --refresh`, rerun `npx -y @aarwitz/tapp@latest pr gate`, then retry `npx -y @aarwitz/tapp@latest pr adopt`";
432
432
  if (!node) throw new Error(`Coverage proposal UI Map node is stale or missing: ${ground.id}; ${refreshAdvice}`);
433
433
  if (target.navigation?.route && !(node.routes || []).some((route) => route.platform === target.platform && route.path === target.navigation.route && route.replayable === true)) {
434
434
  throw new Error(`Coverage proposal route is stale in the persistent UI Map: ${target.navigation.route}; ${refreshAdvice}`);
@@ -79,7 +79,7 @@ function initTargetChoices(model, platform = "") {
79
79
  const candidates = (model.targets || []).filter((target) => !platform || target.platform === platform);
80
80
  return candidates.map((target) => ({
81
81
  target,
82
- command: `tapp init . --explore --platform ${target.platform} --target ${target.sourcePath === "." ? JSON.stringify(target.name) : JSON.stringify(target.sourcePath)}`,
82
+ command: `npx -y @aarwitz/tapp@latest init . --explore --platform ${target.platform} --target ${target.sourcePath === "." ? JSON.stringify(target.name) : JSON.stringify(target.sourcePath)}`,
83
83
  }));
84
84
  }
85
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aarwitz/tapp",
3
- "version": "0.17.0-rc.13",
3
+ "version": "0.17.0-rc.14",
4
4
  "mcpName": "io.github.aarwitz/tapp",
5
5
  "description": "Release contracts, autonomous QA, and evidence-backed CI gates for iOS, Android, and web.",
6
6
  "license": "MIT",
@@ -33,8 +33,9 @@ When the user asks for a general first test of a repository:
33
33
  3. If Tapp returns `target-selection-required`, present its actual choices and ask the user to pick.
34
34
  Never guess among multiple targets. Re-run with the selected platform/target exactly as Tapp
35
35
  instructs.
36
- 4. If a prerequisite is missing, run `tapp doctor`, apply only the stated remediation that is in
37
- scope, and retry once.
36
+ 4. If a prerequisite is missing, call `tapp_health` when MCP is connected or run
37
+ `npx -y @aarwitz/tapp@latest doctor`, apply only the stated remediation that is in scope, and
38
+ retry once.
38
39
 
39
40
  For a focused request, use the requested target directly rather than forcing repository onboarding.
40
41
  Targets may be a repository path, Xcode container, `.app`, iOS bundle id, APK plus Android app id,