@aarwitz/tapp 0.15.1 → 0.16.1

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.
@@ -31,6 +31,7 @@ import { writeHtmlReport } from "./html-report.js";
31
31
  import { buildUiMapFromMarkers, writeUiMap } from "./ui-map.js";
32
32
  import { proposeSelectorMaintenance, validateWebMaintenanceProposal } from "./maintenance-proposal.js";
33
33
  import { isBusinessUiMapNode, releasePlanCandidateFromUiMapNode } from "./application-model.js";
34
+ import { existingProjectArtifactPath } from "./project-paths.js";
34
35
 
35
36
  function parseArgs(argv) {
36
37
  const args = { flowLogs: [], failOn: "gate" };
@@ -201,14 +202,14 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
201
202
  provenance: "runtime-observed",
202
203
  });
203
204
  if (projectDir) {
204
- const releasePlanPath = path.join(path.resolve(projectDir), ".autotap", "release-plan.json");
205
+ const releasePlanPath = existingProjectArtifactPath(path.resolve(projectDir), "release-plan.json");
205
206
  try {
206
207
  const releasePlan = JSON.parse(fs.readFileSync(releasePlanPath, "utf8"));
207
208
  const existing = (releasePlan.items || []).find((candidate) => candidate.id === item.id || candidate.name === item.name ||
208
209
  (candidate.groundedBy || []).some((ground) => ground.type === "ui-map-node" && ground.id === currentNode.id));
209
210
  if (existing) return {
210
211
  existingReleasePlanItem: {
211
- path: ".autotap/release-plan.json",
212
+ path: ".tapp/release-plan.json",
212
213
  id: existing.id,
213
214
  name: existing.name,
214
215
  decision: existing.decision,
@@ -219,7 +220,7 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
219
220
  kind: "release-plan-item-proposal",
220
221
  status: "matches-existing-release-plan",
221
222
  autoApply: false,
222
- targetPath: ".autotap/release-plan.json",
223
+ targetPath: ".tapp/release-plan.json",
223
224
  operation: { op: "reconcile-item", item },
224
225
  reason: "Fresh PR runtime and changed-file evidence can be attached to the existing grounded item only through explicit adoption; its current human decision is preserved.",
225
226
  requiredValidation: "After explicit evidence reconciliation, keep the normal review, generation, deterministic replay, and promotion requirements.",
@@ -231,7 +232,7 @@ function targetCoverageDisposition(target, currentNode, projectDir = "") {
231
232
  kind: "release-plan-item-proposal",
232
233
  status: "awaiting-explicit-adoption",
233
234
  autoApply: false,
234
- targetPath: ".autotap/release-plan.json",
235
+ targetPath: ".tapp/release-plan.json",
235
236
  operation: { op: "add-item", item },
236
237
  reason: `The changed ${currentNode.name} surface was observed in this PR run but is not covered by a selected release contract.`,
237
238
  requiredValidation: "Explicitly adopt and review this item, generate reusable UI-Map-backed Tasks, then replay the resulting contract against the real target before promotion.",
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { existingProjectArtifactPath, projectArtifactDirectory } from "./project-paths.js";
3
4
 
4
5
  function inside(root, candidate) {
5
6
  const value = path.relative(root, candidate);
@@ -36,7 +37,12 @@ export function selectApplicationTarget(model, { platform = "", target = "" } =
36
37
 
37
38
  export function baselinePathForTarget(projectDir, target) {
38
39
  const root = fs.realpathSync(path.resolve(projectDir));
39
- return path.join(root, ".autotap", "baselines", target.platform, `${targetSlug(target.id)}.json`);
40
+ return path.join(root, ".tapp", "baselines", target.platform, `${targetSlug(target.id)}.json`);
41
+ }
42
+
43
+ export function existingBaselinePathForTarget(projectDir, target) {
44
+ const root = fs.realpathSync(path.resolve(projectDir));
45
+ return existingProjectArtifactPath(root, "baselines", target.platform, `${targetSlug(target.id)}.json`);
40
46
  }
41
47
 
42
48
  export function validateBaselineReport(report, { platform, targetId } = {}) {
@@ -134,8 +140,11 @@ function posix(value) {
134
140
  }
135
141
 
136
142
  function suiteDirectories(root, target, kind) {
137
- const candidates = [path.join(root, ".autotap", kind)];
138
- if (target.sourcePath && target.sourcePath !== ".") candidates.push(path.join(root, target.sourcePath, ".autotap", kind));
143
+ const candidates = [path.join(root, projectArtifactDirectory(root), kind)];
144
+ if (target.sourcePath && target.sourcePath !== ".") {
145
+ const targetRoot = path.join(root, target.sourcePath);
146
+ candidates.push(path.join(targetRoot, projectArtifactDirectory(targetRoot), kind));
147
+ }
139
148
  return [...new Set(candidates)].filter((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
140
149
  .map((candidate) => `${posix(path.relative(root, candidate))}/*.yml`);
141
150
  }
@@ -176,7 +185,7 @@ function credentialConfiguration(model, targetContracts = []) {
176
185
  if (requirements.has("email")) bindings.add("TAPP_TEST_EMAIL");
177
186
  if (requirements.has("password")) bindings.add("TAPP_TEST_PASSWORD");
178
187
  }
179
- 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 .autotap/project.json`);
188
+ 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`);
180
189
  const inputs = {};
181
190
  const primaryEmail = primary.credentialBindings?.email || (!primary.credentialBindings && requirements.has("email") ? "TAPP_TEST_EMAIL" : "");
182
191
  const primaryPassword = primary.credentialBindings?.password || (!primary.credentialBindings && requirements.has("password") ? "TAPP_TEST_PASSWORD" : "");
@@ -220,7 +229,7 @@ function targetInputs(root, model, target) {
220
229
  if (flows.length) inputs.flows = flows.join(" ");
221
230
  if (scenarios.length) inputs.scenarios = scenarios.join(" ");
222
231
  if (contracts.length) inputs.contracts = contracts.join(" ");
223
- const baselinePath = baselinePathForTarget(root, target);
232
+ const baselinePath = existingBaselinePathForTarget(root, target);
224
233
  if (fs.existsSync(baselinePath)) inputs.baseline = posix(path.relative(root, baselinePath));
225
234
  const credentials = credentialConfiguration(model, targetContracts);
226
235
  Object.assign(inputs, credentials.inputs);
@@ -292,7 +301,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
292
301
  jobs.push(lines.join("\n"));
293
302
  }
294
303
  const workflow = [
295
- "# Generated by `tapp ci install` from .autotap/application-model.json.",
304
+ "# Generated by `tapp ci install` from .tapp/application-model.json.",
296
305
  "# Review this patch. Tapp never overwrites it silently.",
297
306
  "name: Tapp release gate",
298
307
  "",
@@ -341,7 +350,7 @@ export function renderGithubWorkflow({ projectDir, model, actionRef, defaultBran
341
350
  return { workflow, manifest };
342
351
  }
343
352
 
344
- export function writeCiInstallation({ projectDir, workflow, manifest, workflowPath = ".github/workflows/tapp.yml", manifestPath = ".autotap/ci.json", replace = false } = {}) {
353
+ export function writeCiInstallation({ projectDir, workflow, manifest, workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false } = {}) {
345
354
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
346
355
  const destinations = [path.resolve(root, workflowPath), path.resolve(root, manifestPath)];
347
356
  for (const destination of destinations) if (!inside(root, destination)) throw new Error("CI installation outputs must remain inside the repository");
@@ -13,6 +13,7 @@ import {
13
13
  } from "@modelcontextprotocol/sdk/types.js";
14
14
 
15
15
  import { parseOcqaMarkers, buildQaReport, computeRegression } from "./report.js";
16
+ import { existingProjectArtifactPath, projectArtifactDirectory } from "./project-paths.js";
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
@@ -918,7 +919,7 @@ export async function saveInteractiveSessionFlow({ projectDir, name, addFinalAss
918
919
  steps,
919
920
  };
920
921
  const slug = flowName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "flow";
921
- const dir = path.join(root, ".autotap", "flows");
922
+ const dir = path.join(root, ".tapp", "flows");
922
923
  const outPath = path.join(dir, `${slug}.yml`);
923
924
  if (fs.existsSync(outPath) && !replace) {
924
925
  const error = new Error(`Flow '${path.relative(root, outPath)}' already exists. Choose another name or explicitly replace it.`);
@@ -1589,7 +1590,7 @@ export async function runQaIos({ bundleId, maxActions, timeout, args = {}, onPro
1589
1590
  export async function runInitExploration({
1590
1591
  projectDir,
1591
1592
  platform,
1592
- outDir = ".autotap",
1593
+ outDir = ".tapp",
1593
1594
  url = "",
1594
1595
  target = "",
1595
1596
  bundleId = "",
@@ -1610,7 +1611,7 @@ export async function runInitExploration({
1610
1611
  catch { return { error: `Repository directory not found: ${projectDir || process.cwd()}` }; }
1611
1612
  const selected = String(platform || (url ? "web" : appId || apkPath ? "android" : "ios")).toLowerCase();
1612
1613
  if (!["ios", "android", "web"].includes(selected)) return { error: "platform must be ios|android|web" };
1613
- const mapPath = path.resolve(root, outDir, "ui-map.json");
1614
+ const mapPath = path.resolve(root, projectArtifactDirectory(root, outDir), "ui-map.json");
1614
1615
  if (!isInsideDir(root, mapPath)) return { error: "UI Map output must remain inside the repository" };
1615
1616
 
1616
1617
  let resolvedTarget = "";
@@ -2152,7 +2153,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2152
2153
  testEmail: { type: "string", description: "Explore: actor/login email; never persisted in the model" },
2153
2154
  testPassword: { type: "string", description: "Explore: actor/login password; never persisted in the model" },
2154
2155
  maxContracts: { type: "integer", minimum: 1, maximum: 50, default: 15 },
2155
- outDir: { type: "string", description: "Repo-relative artifact directory; default .autotap" },
2156
+ outDir: { type: "string", description: "Repo-relative artifact directory; default .tapp" },
2156
2157
  },
2157
2158
  },
2158
2159
  },
@@ -2160,7 +2161,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2160
2161
  name: "tapp_actor_config",
2161
2162
  title: "Inspect or configure named test actors without storing credential values",
2162
2163
  description:
2163
- "Manage the repository-native .autotap/project.json actor/session contract used by init, release-contract generation, and CI. `read` is inspect-only. `set` writes an explicit actor role, isolation/provisioning policy, and credential-name to environment-variable-name bindings. The tool never accepts, returns, or persists credential values and never overwrites an actor unless replace is explicit.",
2164
+ "Manage the repository-native .tapp/project.json actor/session contract used by init, release-contract generation, and CI. `read` is inspect-only. `set` writes an explicit actor role, isolation/provisioning policy, and credential-name to environment-variable-name bindings. The tool never accepts, returns, or persists credential values and never overwrites an actor unless replace is explicit.",
2164
2165
  inputSchema: {
2165
2166
  type: "object",
2166
2167
  properties: {
@@ -2180,14 +2181,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2180
2181
  name: "tapp_release_plan",
2181
2182
  title: "Inspect or explicitly review a Tapp release plan",
2182
2183
  description:
2183
- "Read the repository-native release plan, apply explicit approve/reject/defer decisions, generate grounded Task/contract drafts, deterministically validate drafts on a real target, or explicitly promote fully replay-validated drafts into reviewed repository-native artifacts. Review changes only decision metadata. Generation writes under .autotap/proposals, never overwrites, never invokes AI, and remains untrusted until real deterministic replay passes. Web validation can build/start/stop the detected managed target when url is omitted.",
2184
+ "Read the repository-native release plan, apply explicit approve/reject/defer decisions, generate grounded Task/contract drafts, deterministically validate drafts on a real target, or explicitly promote fully replay-validated drafts into reviewed repository-native artifacts. Review changes only decision metadata. Generation writes under .tapp/proposals, never overwrites, never invokes AI, and remains untrusted until real deterministic replay passes. Web validation can build/start/stop the detected managed target when url is omitted.",
2184
2185
  inputSchema: {
2185
2186
  type: "object",
2186
2187
  properties: {
2187
2188
  authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
2188
2189
  operation: { type: "string", enum: ["read", "review", "generate", "validate", "promote"], default: "read" },
2189
- planPath: { type: "string", description: "Repo-relative plan path; default .autotap/release-plan.json" },
2190
- projectDir: { type: "string", description: "Generate: repo-relative project root containing the scoped .autotap Task directories" },
2190
+ planPath: { type: "string", description: "Repo-relative plan path; default .tapp/release-plan.json" },
2191
+ projectDir: { type: "string", description: "Generate: repo-relative project root containing the scoped .tapp Task directories" },
2191
2192
  approve: { type: "array", items: { type: "string" }, description: "Plan item ids or names to approve" },
2192
2193
  reject: { type: "array", items: { type: "string" }, description: "Plan item ids or names to reject" },
2193
2194
  defer: { type: "array", items: { type: "string" }, description: "Plan item ids or names to defer" },
@@ -2214,11 +2215,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2214
2215
  authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
2215
2216
  operation: { type: "string", enum: ["inspect", "install", "baseline"], default: "inspect" },
2216
2217
  projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
2217
- modelPath: { type: "string", description: "Repo-relative application model path; defaults to <projectDir>/.autotap/application-model.json" },
2218
+ modelPath: { type: "string", description: "Repo-relative application model path; defaults to <projectDir>/.tapp/application-model.json" },
2218
2219
  actionRef: { type: "string", description: "GitHub Action reference owner/repository@release-tag-or-sha; defaults to the current Tapp release tag" },
2219
2220
  defaultBranch: { type: "string", default: "main" },
2220
2221
  workflowPath: { type: "string", description: "Install: project-relative output; default .github/workflows/tapp.yml" },
2221
- manifestPath: { type: "string", description: "Install: project-relative output; default .autotap/ci.json" },
2222
+ manifestPath: { type: "string", description: "Install: project-relative output; default .tapp/ci.json" },
2222
2223
  allowUnresolved: { type: "boolean", default: false, description: "Permit writing a draft whose manifest names unresolved target configuration" },
2223
2224
  replace: { type: "boolean", default: false, description: "Explicitly replace an existing generated workflow/manifest or target baseline" },
2224
2225
  reportPath: { type: "string", description: "Baseline: repo-relative successful conclusive portable-gate JSON report" },
@@ -2240,7 +2241,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2240
2241
  authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
2241
2242
  operation: { type: "string", enum: ["read", "build", "diff"], default: "read" },
2242
2243
  captureId: { type: "string", description: "Read/build from this Tapp capture's ocqa-markers.txt/ui-map.json" },
2243
- mapPath: { type: "string", description: "Repo-relative UI Map path for read, or build output (default .autotap/ui-map.json)" },
2244
+ mapPath: { type: "string", description: "Repo-relative UI Map path for read, or build output (default .tapp/ui-map.json)" },
2244
2245
  markersPath: { type: "string", description: "Repo-relative OCQA markers path for build when captureId is not supplied" },
2245
2246
  beforePath: { type: "string", description: "Repo-relative baseline UI Map for diff" },
2246
2247
  afterPath: { type: "string", description: "Repo-relative current UI Map for diff" },
@@ -2255,7 +2256,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2255
2256
  name: "tapp_task",
2256
2257
  title: "Inspect, validate, or compile a reusable deterministic Task",
2257
2258
  description:
2258
- "Work with repository-native compositional Tasks in .autotap/tasks. Tasks define inputs, outputs, pre/postconditions, platform implementations, and the UI Map states/transitions they cover. " +
2259
+ "Work with repository-native compositional Tasks in .tapp/tasks. Tasks define inputs, outputs, pre/postconditions, platform implementations, and the UI Map states/transitions they cover. " +
2259
2260
  "Validation is deterministic and can ground selectors/coverage against ui-map.json. Compilation expands a Task into the shared keyless Flow contract with reviewable Task provenance; pass that returned flow to tapp_flow_run to replay it. No AI or API key is used.",
2260
2261
  inputSchema: {
2261
2262
  type: "object",
@@ -2263,7 +2264,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2263
2264
  properties: {
2264
2265
  authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
2265
2266
  operation: { type: "string", enum: ["read", "validate", "compile"], default: "validate" },
2266
- taskPath: { type: "string", description: "Repo-relative .autotap/tasks/*.yml|json file" },
2267
+ taskPath: { type: "string", description: "Repo-relative .tapp/tasks/*.yml|json file" },
2267
2268
  platform: { type: "string", enum: ["ios", "android", "web"], description: "Implementation to validate/compile" },
2268
2269
  inputs: { type: "object", additionalProperties: { type: "string" }, description: "Task inputs for compile. Secret inputs must be environment placeholders such as $TEST_PASSWORD, never plaintext." },
2269
2270
  mapPath: { type: "string", description: "Optional repo-relative UI Map v1 used to ground states, edges, and semantic controls" },
@@ -2276,7 +2277,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2276
2277
  name: "tapp_release_contract",
2277
2278
  title: "Inspect, validate, compile, or run a release contract",
2278
2279
  description:
2279
- "Work with repository-native TypeScript release contracts in .autotap/contracts. Contracts express business guarantees through reusable Tasks, named actors, exact/eventual expectations, criticality, policy, and UI Map coverage. " +
2280
+ "Work with repository-native TypeScript release contracts in .tapp/contracts. Contracts express business guarantees through reusable Tasks, named actors, exact/eventual expectations, criticality, policy, and UI Map coverage. " +
2280
2281
  "Compilation targets the same deterministic Flow/Scenario evidence contract; ordinary run is keyless and never invokes a model. Multi-actor isolated replay is currently web-only.",
2281
2282
  inputSchema: {
2282
2283
  type: "object",
@@ -2284,7 +2285,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2284
2285
  properties: {
2285
2286
  authToken: { type: "string", description: "Required when TAPP_MCP_TOKEN is set" },
2286
2287
  operation: { type: "string", enum: ["read", "validate", "compile", "run"], default: "validate" },
2287
- contractPath: { type: "string", description: "Repo-relative .autotap/contracts/*.contract.ts file" },
2288
+ contractPath: { type: "string", description: "Repo-relative .tapp/contracts/*.contract.ts file" },
2288
2289
  platform: { type: "string", enum: ["ios", "android", "web"], description: "Target platform; optional when the contract declares exactly one" },
2289
2290
  mapPath: { type: "string", description: "Optional repo-relative UI Map v1 for coverage grounding" },
2290
2291
  updateMap: { type: "boolean", default: false, description: "Explicitly add the reviewed contract coverage to mapPath" },
@@ -2326,10 +2327,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2326
2327
  },
2327
2328
  projectDir: { type: "string", description: "Repo-relative project root; defaults to the MCP workspace root" },
2328
2329
  platform: { type: "string", enum: ["ios", "android", "web"], description: "Optional platform filter" },
2329
- mapPath: { type: "string", description: "Project-relative UI Map; defaults to .autotap/ui-map.json" },
2330
+ mapPath: { type: "string", description: "Project-relative UI Map; defaults to .tapp/ui-map.json" },
2330
2331
  prPlanPath: { type: "string", description: "Adopt: project-relative executed PR plan containing conclusive exploration evidence" },
2331
2332
  item: { type: "string", description: "Adopt: stable exploration target id whose reviewable proposal should be appended" },
2332
- releasePlanPath: { type: "string", description: "Adopt: project-relative target; defaults to .autotap/release-plan.json" },
2333
+ releasePlanPath: { type: "string", description: "Adopt: project-relative target; defaults to .tapp/release-plan.json" },
2333
2334
  },
2334
2335
  },
2335
2336
  },
@@ -2356,7 +2357,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2356
2357
  description:
2357
2358
  "Inline Flow: {name, app, steps:[...], vars?}. Example: {name:'login', app:'com.acme.app', steps:[{tap:'Sign In'}, {type:{field:'Email', value:'$TEST_EMAIL'}}, {tap:'Continue'}, {assert_screen:'Home'}]}",
2358
2359
  },
2359
- flowPath: { type: "string", description: "Alternative to `flow`: repo-relative path to a .yml/.json Flow (e.g. .autotap/flows/login.yml)" },
2360
+ flowPath: { type: "string", description: "Alternative to `flow`: repo-relative path to a .yml/.json Flow (e.g. .tapp/flows/login.yml)" },
2360
2361
  platform: { type: "string", enum: ["ios", "web", "android"], description: "Overrides Flow platform detection" },
2361
2362
  appBundleId: { type: "string", description: "iOS: overrides the Flow's `app:` field" },
2362
2363
  androidAppId: { type: "string", description: "Android: overrides the Flow's `app:` field" },
@@ -2394,7 +2395,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2394
2395
  "Write a deterministic E2E Flow from a natural-language goal (e.g. 'sign in and open Settings'), " +
2395
2396
  "GROUNDED in the app's real screens so it can't invent steps. Tapp explores the app to build a " +
2396
2397
  "screen/control map (or reuses a recent run via captureId), then a model authors a Flow using only " +
2397
- "screens/controls that were actually observed. Saves it to .autotap/flows/<name>.yml and returns the " +
2398
+ "screens/controls that were actually observed. Saves it to .tapp/flows/<name>.yml and returns the " +
2398
2399
  "YAML for review (optionally runs it). Needs a model backend (Tapp subscription token or " +
2399
2400
  "ANTHROPIC_API_KEY). Use this to bootstrap a test you then refine; use tapp_flow_run to replay it.",
2400
2401
  inputSchema: {
@@ -2418,7 +2419,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2418
2419
  description:
2419
2420
  "Save what you've done in the CURRENT interactive session as a reusable, deterministic Flow " +
2420
2421
  "(record-by-doing). Every successful tapp_session_act (tap/type/swipe/back) is recorded; this " +
2421
- "writes them to .autotap/flows/<name>.yml with wait_for steps auto-inserted on screen changes and a " +
2422
+ "writes them to .tapp/flows/<name>.yml with wait_for steps auto-inserted on screen changes and a " +
2422
2423
  "final assert_screen checkpoint. Typed credentials are templated to $TEST_EMAIL/$TEST_PASSWORD so the " +
2423
2424
  "flow is shareable. The saved flow replays with tapp_flow_run. Do it once → it's a test.",
2424
2425
  inputSchema: {
@@ -2908,7 +2909,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2908
2909
  if (maxContracts < 1 || maxContracts > 50) return errorResult("maxContracts must be between 1 and 50");
2909
2910
  const { initializeProductProject } = await import("./product-operations.js");
2910
2911
  try {
2911
- const outDir = isNonEmptyString(args.outDir) ? args.outDir.trim() : ".autotap";
2912
+ const outDir = isNonEmptyString(args.outDir) ? args.outDir.trim() : ".tapp";
2912
2913
  const resolvedOut = path.resolve(projectDir, outDir);
2913
2914
  if (!isInsideDir(projectDir, resolvedOut)) return errorResult("outDir must be inside projectDir");
2914
2915
  const selectedPlatform = isNonEmptyString(args.platform) ? args.platform.trim().toLowerCase()
@@ -2979,7 +2980,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2979
2980
  if (unauthorized) return unauthorized;
2980
2981
  const operation = String(args.operation || "read").toLowerCase();
2981
2982
  if (!["read", "review", "generate", "validate", "promote"].includes(operation)) return errorResult("operation must be read|review|generate|validate|promote");
2982
- const planPath = path.resolve(repoRoot, isNonEmptyString(args.planPath) ? args.planPath.trim() : ".autotap/release-plan.json");
2983
+ const planPath = isNonEmptyString(args.planPath) ? path.resolve(repoRoot, args.planPath.trim()) : existingProjectArtifactPath(repoRoot, "release-plan.json");
2983
2984
  if (!isInsideDir(repoRoot, planPath)) return errorResult("planPath must be inside the repo");
2984
2985
  if (!fs.existsSync(planPath)) return errorResult("Release plan not found", { planPath });
2985
2986
  let plan;
@@ -3054,7 +3055,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3054
3055
  if (!["inspect", "install", "baseline"].includes(operation)) return errorResult("operation must be inspect|install|baseline");
3055
3056
  const projectDir = isNonEmptyString(args.projectDir) ? path.resolve(repoRoot, args.projectDir.trim()) : repoRoot;
3056
3057
  if (!isInsideDir(repoRoot, projectDir) || !fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) return errorResult("projectDir must be an existing directory inside the repo");
3057
- const modelPath = isNonEmptyString(args.modelPath) ? path.resolve(repoRoot, args.modelPath.trim()) : path.join(projectDir, ".autotap", "application-model.json");
3058
+ const modelPath = isNonEmptyString(args.modelPath) ? path.resolve(repoRoot, args.modelPath.trim()) : existingProjectArtifactPath(projectDir, "application-model.json");
3058
3059
  if (!isInsideDir(projectDir, modelPath) || !fs.existsSync(modelPath)) return errorResult("Application model not found inside projectDir; run tapp_init first", { modelPath });
3059
3060
  let model;
3060
3061
  try { model = JSON.parse(fs.readFileSync(modelPath, "utf8")); }
@@ -3078,7 +3079,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3078
3079
  const rendered = prepareProductCi({ projectDir, modelPath, actionRef, defaultBranch });
3079
3080
  if (operation === "inspect") return richResult(`🧩 CI plan — ${rendered.manifest.targets.length} target job(s) · ${rendered.manifest.unresolved.length} unresolved · read-only`, rendered);
3080
3081
  if (rendered.manifest.unresolved.length && !asBoolean(args.allowUnresolved)) return errorResult("CI workflow not installed because target configuration remains unresolved", { unresolved: rendered.manifest.unresolved, next: "Resolve the application model requirements or explicitly allow an inspect-only draft." });
3081
- const result = installProductCi({ projectDir, modelPath, actionRef, defaultBranch, workflowPath: isNonEmptyString(args.workflowPath) ? args.workflowPath.trim() : ".github/workflows/tapp.yml", manifestPath: isNonEmptyString(args.manifestPath) ? args.manifestPath.trim() : ".autotap/ci.json", replace: asBoolean(args.replace), allowUnresolved: asBoolean(args.allowUnresolved) });
3082
+ const result = installProductCi({ projectDir, modelPath, actionRef, defaultBranch, workflowPath: isNonEmptyString(args.workflowPath) ? args.workflowPath.trim() : ".github/workflows/tapp.yml", manifestPath: isNonEmptyString(args.manifestPath) ? args.manifestPath.trim() : ".tapp/ci.json", replace: asBoolean(args.replace), allowUnresolved: asBoolean(args.allowUnresolved) });
3082
3083
  return richResult(`✅ Reviewable CI gate installed — ${result.manifest.targets.length} target job(s); no commit, push, branch protection, or GitHub resource was created`, result);
3083
3084
  } catch (error) { return errorResult("Could not prepare CI installation", { detail: error.message || String(error) }); }
3084
3085
  }
@@ -3109,7 +3110,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3109
3110
  const markersPath = capture ? path.join(capture.path, "ocqa-markers.txt") : resolveRepoFile(args.markersPath);
3110
3111
  if (!markersPath) return errorResult("markersPath must be inside the repo, or provide captureId");
3111
3112
  if (!fs.existsSync(markersPath)) return errorResult("OCQA markers not found", { markersPath });
3112
- const outPath = resolveRepoFile(args.mapPath, path.join(".autotap", "ui-map.json"));
3113
+ const outPath = isNonEmptyString(args.mapPath) ? resolveRepoFile(args.mapPath) : existingProjectArtifactPath(repoRoot, "ui-map.json");
3113
3114
  if (!outPath) return errorResult("mapPath must be inside the repo");
3114
3115
  try {
3115
3116
  const observed = buildUiMapFromMarkers({ markersPath, platform: args.platform || "ios", target: args.target || "", runId: capture?.id || "" });
@@ -3120,7 +3121,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3120
3121
  } catch (error) { return errorResult("Could not build UI Map", { detail: error.message || String(error) }); }
3121
3122
  }
3122
3123
  if (operation !== "read") return errorResult("operation must be read|build|diff");
3123
- const mapPath = capture ? path.join(capture.path, "ui-map.json") : resolveRepoFile(args.mapPath, path.join(".autotap", "ui-map.json"));
3124
+ const mapPath = capture ? path.join(capture.path, "ui-map.json") : isNonEmptyString(args.mapPath) ? resolveRepoFile(args.mapPath) : existingProjectArtifactPath(repoRoot, "ui-map.json");
3124
3125
  if (!mapPath) return errorResult("mapPath must be inside the repo");
3125
3126
  if (!fs.existsSync(mapPath)) return errorResult("UI Map not found; run QA or operation=build first", { mapPath });
3126
3127
  try {
@@ -3480,7 +3481,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3480
3481
  const ungrounded = ungroundedScreens(parsed.steps, grounding);
3481
3482
  const flow = { name: args.name || parsed.name, app: bundleId, steps: parsed.steps };
3482
3483
  const slug = flow.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "generated-flow";
3483
- const dir = path.join(repoRoot, ".autotap", "flows");
3484
+ const dir = path.join(repoRoot, ".tapp", "flows");
3484
3485
  fs.mkdirSync(dir, { recursive: true });
3485
3486
  const outPath = path.join(dir, `${slug}.yml`);
3486
3487
  const yamlRes = await runCommand("python3", [path.join(scriptsDir, "flow_lib.py"), "to-yaml", JSON.stringify(flow)], { cwd: repoRoot });
@@ -125,16 +125,16 @@ export async function validateWebMaintenanceProposal({ proposal, projectDir, url
125
125
  if (!projectDir || !url) throw new Error("web maintenance validation requires projectDir and the running target URL");
126
126
  const root = fs.realpathSync(path.resolve(projectDir));
127
127
  const operation = proposal.operations[0];
128
- const taskRoot = fs.realpathSync(path.join(root, ".autotap", "tasks"));
128
+ const taskRoot = fs.realpathSync(path.join(root, ".tapp", "tasks"));
129
129
  const sourceTask = fs.realpathSync(path.resolve(root, operation.taskPath));
130
130
  const sourceContract = fs.realpathSync(path.resolve(root, proposal.contractIntent.path));
131
- if (!inside(taskRoot, sourceTask)) throw new Error("maintenance Task must be a regular reviewed file under .autotap/tasks");
131
+ if (!inside(taskRoot, sourceTask)) throw new Error("maintenance Task must be a regular reviewed file under .tapp/tasks");
132
132
  if (!inside(root, sourceContract)) throw new Error("maintenance contract must remain inside the project");
133
133
  if (digest(sourceTask) !== operation.taskSha256) throw new Error("Task digest changed after the proposal was created");
134
134
  if (digest(sourceContract) !== proposal.contractIntent.sha256) throw new Error("release-contract intent digest changed after the proposal was created");
135
135
 
136
136
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tapp-maintenance-validation-"));
137
- const tempTasks = path.join(tempRoot, ".autotap", "tasks");
137
+ const tempTasks = path.join(tempRoot, ".tapp", "tasks");
138
138
  const stem = String(proposal.contractIntent.name || "contract").replace(/[^A-Za-z0-9._-]/g, "-");
139
139
  const outputDir = evidenceDir ? path.resolve(evidenceDir, stem) : path.join(tempRoot, "evidence");
140
140
  const logPath = path.join(outputDir, "validation.log");
@@ -153,7 +153,7 @@ export async function validateWebMaintenanceProposal({ proposal, projectDir, url
153
153
  if (!(contract.setup || []).length || !(contract.teardown || []).length) {
154
154
  throw new Error("automatic disposable maintenance validation requires controlled contract setup and teardown");
155
155
  }
156
- const pseudoContractPath = path.join(tempRoot, ".autotap", "contracts", path.basename(sourceContract));
156
+ const pseudoContractPath = path.join(tempRoot, ".tapp", "contracts", path.basename(sourceContract));
157
157
  const execution = compileReleaseContract(contract, { platform: "web", sourcePath: pseudoContractPath });
158
158
  const result = await runWebFlow({ flow: execution, url, logPath, screenshotDir: outputDir });
159
159
  const contractUnchanged = digest(sourceContract) === proposal.contractIntent.sha256;
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
8
8
  import { loadReleaseContractFile } from "./release-contract.js";
9
9
  import { loadTaskRegistry } from "./task-runtime.js";
10
10
  import { replayableUiMapNavigation, semanticUiKey } from "./ui-map.js";
11
+ import { existingProjectArtifactPath, isProjectArtifactDirectory } from "./project-paths.js";
11
12
 
12
13
  function posix(value) {
13
14
  return String(value || "").replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
@@ -28,8 +29,8 @@ export function sourcePathMatches(changedFile, ownershipPath) {
28
29
  function repoRootFor(sourcePath) {
29
30
  let current = path.dirname(path.resolve(sourcePath));
30
31
  while (current !== path.dirname(current)) {
31
- if (path.basename(current) === ".autotap") return path.dirname(current);
32
- if (fs.existsSync(path.join(current, ".autotap"))) return current;
32
+ if (isProjectArtifactDirectory(path.basename(current))) return path.dirname(current);
33
+ if (fs.existsSync(existingProjectArtifactPath(current))) return current;
33
34
  current = path.dirname(current);
34
35
  }
35
36
  return process.cwd();
@@ -403,11 +404,11 @@ function mergeGroundingEvidence(existing, incoming) {
403
404
  return merged;
404
405
  }
405
406
 
406
- export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".autotap/release-plan.json" } = {}) {
407
+ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releasePlanPath = ".tapp/release-plan.json" } = {}) {
407
408
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
408
409
  const source = path.resolve(prPlanPath || "");
409
410
  if (!prPlanPath || !fs.existsSync(source)) throw new Error(`PR plan not found: ${source || "(missing path)"}`);
410
- const targetPath = path.resolve(root, releasePlanPath);
411
+ const targetPath = releasePlanPath === ".tapp/release-plan.json" ? existingProjectArtifactPath(root, "release-plan.json") : path.resolve(root, releasePlanPath);
411
412
  if (!inside(root, targetPath)) throw new Error("Release plan path must stay inside the project directory");
412
413
  if (!fs.existsSync(targetPath)) throw new Error(`Release plan not found: ${targetPath}; run tapp init first`);
413
414
  const prPlan = JSON.parse(fs.readFileSync(source, "utf8"));
@@ -423,7 +424,7 @@ export function adoptPrCoverageProposal({ projectDir, prPlanPath, item, releaseP
423
424
  const proposed = structuredClone(proposal.operation.item);
424
425
  if (proposed?.origin !== "deterministic-ui-map-proposal" || proposed?.decision !== "pending") throw new Error("Coverage proposal is not a pending UI-Map-grounded release-plan item");
425
426
  const ground = (proposed.groundedBy || []).find((entry) => entry.type === "ui-map-node");
426
- const mapPath = path.join(root, ".autotap", "ui-map.json");
427
+ const mapPath = existingProjectArtifactPath(root, "ui-map.json");
427
428
  if (!ground || !fs.existsSync(mapPath)) throw new Error("Coverage proposal requires the repository's persistent UI Map");
428
429
  const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
429
430
  const node = (map.nodes || []).find((candidate) => candidate.id === ground.id && candidate.status !== "proposed");
@@ -583,7 +584,7 @@ export async function buildPrContractPlan({
583
584
  ...(Array.isArray(changedSymbolEvidence) ? changedSymbolEvidence : []),
584
585
  ]);
585
586
  const evidenceByFile = new Map(diffEvidence.map((item) => [item.file, item]));
586
- const contractDir = path.join(root, ".autotap", "contracts");
587
+ const contractDir = existingProjectArtifactPath(root, "contracts");
587
588
  const discovered = discoverContracts && fs.existsSync(contractDir)
588
589
  ? fs.readdirSync(contractDir).filter((name) => /\.contract\.(?:ts|mts|mjs|js|json)$/i.test(name)).map((name) => path.join(contractDir, name)) : [];
589
590
  const files = [...new Set([...discovered, ...contractPaths.map((item) => path.resolve(root, item))])];
@@ -594,10 +595,10 @@ export async function buildPrContractPlan({
594
595
  }
595
596
 
596
597
  let tasks = new Map();
597
- const registrySource = files[0] || path.join(root, ".autotap", "contracts", "contract.ts");
598
+ const registrySource = files[0] || existingProjectArtifactPath(root, "contracts", "contract.ts");
598
599
  try { tasks = loadTaskRegistry({ sourcePath: registrySource, projectDir: root }); } catch {}
599
600
  let map = null;
600
- const resolvedMapPath = mapPath ? path.resolve(root, mapPath) : path.join(root, ".autotap", "ui-map.json");
601
+ const resolvedMapPath = mapPath ? path.resolve(root, mapPath) : existingProjectArtifactPath(root, "ui-map.json");
601
602
  if (fs.existsSync(resolvedMapPath)) map = JSON.parse(fs.readFileSync(resolvedMapPath, "utf8"));
602
603
 
603
604
  const impactedNodes = map ? map.nodes.filter((node) => matchedFiles(changes, node.sourcePaths || []).length) : [];
@@ -16,8 +16,9 @@ import {
16
16
  reviewReleasePlan,
17
17
  writeInitArtifacts,
18
18
  } from "./application-model.js";
19
- import { baselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
19
+ import { baselinePathForTarget, existingBaselinePathForTarget, renderGithubWorkflow, selectApplicationTarget, writeCiInstallation, writeTargetBaseline } from "./ci-setup.js";
20
20
  import { executeReleaseContract, runProductProcess } from "./product-execution.js";
21
+ import { projectArtifactDirectory } from "./project-paths.js";
21
22
 
22
23
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
23
24
  const packageVersion = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version;
@@ -50,8 +51,9 @@ function atomicJson(file, value) {
50
51
  fs.renameSync(temporary, file);
51
52
  }
52
53
 
53
- function artifactPaths(root, outDir = ".autotap") {
54
- const dir = path.resolve(root, outDir);
54
+ function artifactPaths(root, outDir = ".tapp") {
55
+ const selectedOutDir = projectArtifactDirectory(root, outDir);
56
+ const dir = path.resolve(root, selectedOutDir);
55
57
  if (!inside(root, dir)) throw new Error("Artifact directory must remain inside the repository");
56
58
  return {
57
59
  dir,
@@ -94,7 +96,7 @@ function listProductRuns(root) {
94
96
  }
95
97
 
96
98
  function listRepositoryFlows(root) {
97
- const directory = path.join(root, ".autotap", "flows");
99
+ const directory = path.join(root, projectArtifactDirectory(root), "flows");
98
100
  if (!fs.existsSync(directory)) return [];
99
101
  return fs.readdirSync(directory, { withFileTypes:true })
100
102
  .filter((entry) => entry.isFile() && /\.(?:ya?ml|json)$/i.test(entry.name))
@@ -127,7 +129,7 @@ function listRepositoryFlows(root) {
127
129
  .sort((left, right) => left.path.localeCompare(right.path));
128
130
  }
129
131
 
130
- export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
132
+ export function readProductProject({ projectDir, outDir = ".tapp" } = {}) {
131
133
  const root = realProject(projectDir);
132
134
  const paths = artifactPaths(root, outDir);
133
135
  const model = readJson(paths.model);
@@ -137,7 +139,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
137
139
  const requirements = model?.requirements || [];
138
140
  const planItems = plan?.items || [];
139
141
  const baselines = (model?.targets || []).map((target) => {
140
- const file = baselinePathForTarget(root, target);
142
+ const file = existingBaselinePathForTarget(root, target);
141
143
  return { targetId: target.id, platform: target.platform, path: file, relativePath: path.relative(root, file).replaceAll(path.sep, "/"), exists: fs.existsSync(file) };
142
144
  });
143
145
  return {
@@ -177,7 +179,7 @@ export function readProductProject({ projectDir, outDir = ".autotap" } = {}) {
177
179
  // target identity or configuration rules.
178
180
  export async function prepareProductTarget({
179
181
  projectDir,
180
- outDir = ".autotap",
182
+ outDir = ".tapp",
181
183
  platform = "",
182
184
  target = "",
183
185
  appPath = "",
@@ -262,7 +264,7 @@ export async function prepareProductTarget({
262
264
  export async function initializeProductProject({
263
265
  projectDir,
264
266
  mode = "inspect",
265
- outDir = ".autotap",
267
+ outDir = ".tapp",
266
268
  ownedUrl = "",
267
269
  platform = "",
268
270
  target = "",
@@ -326,7 +328,7 @@ function resolvePlan(root, outDir, planPath = "") {
326
328
  return { file, plan: readJson(file, { required: true }) };
327
329
  }
328
330
 
329
- export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath = "", approve = [], reject = [], defer = [] } = {}) {
331
+ export function reviewProductPlan({ projectDir, outDir = ".tapp", planPath = "", approve = [], reject = [], defer = [] } = {}) {
330
332
  const root = realProject(projectDir);
331
333
  const resolved = resolvePlan(root, outDir, planPath);
332
334
  const plan = reviewReleasePlan(resolved.plan, { approve, reject, defer });
@@ -334,7 +336,7 @@ export function reviewProductPlan({ projectDir, outDir = ".autotap", planPath =
334
336
  return { operation: "review-plan", plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
335
337
  }
336
338
 
337
- export async function generateProductPlan({ projectDir, outDir = ".autotap", planPath = "" } = {}) {
339
+ export async function generateProductPlan({ projectDir, outDir = ".tapp", planPath = "" } = {}) {
338
340
  const root = realProject(projectDir);
339
341
  const resolved = resolvePlan(root, outDir, planPath);
340
342
  const result = await generateApprovedContractProposals(resolved.plan, { projectDir: root });
@@ -344,7 +346,7 @@ export async function generateProductPlan({ projectDir, outDir = ".autotap", pla
344
346
 
345
347
  export async function validateProductPlan({
346
348
  projectDir,
347
- outDir = ".autotap",
349
+ outDir = ".tapp",
348
350
  planPath = "",
349
351
  items = [],
350
352
  platform = "",
@@ -411,7 +413,7 @@ export async function validateProductPlan({
411
413
  return { operation: "validate-plan", platform: selectedPlatform, passed: results.every((item) => item.passed), results, plan, planPath: resolved.file, project: readProductProject({ projectDir: root, outDir }) };
412
414
  }
413
415
 
414
- export async function promoteProductPlan({ projectDir, outDir = ".autotap", planPath = "", items = [] } = {}) {
416
+ export async function promoteProductPlan({ projectDir, outDir = ".tapp", planPath = "", items = [] } = {}) {
415
417
  const root = realProject(projectDir);
416
418
  const resolved = resolvePlan(root, outDir, planPath);
417
419
  const result = await promoteValidatedProposals(resolved.plan, { projectDir: root, ids: items || [] });
@@ -423,7 +425,7 @@ export async function promoteProductPlan({ projectDir, outDir = ".autotap", plan
423
425
  return { operation: "promote-plan", ...result, plan: written.plan, planPath: written.planPath, modelPath: written.modelPath, project: readProductProject({ projectDir: root, outDir }) };
424
426
  }
425
427
 
426
- export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
428
+ export function prepareProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main" } = {}) {
427
429
  const root = realProject(projectDir);
428
430
  const modelFile = modelPath ? path.resolve(root, modelPath) : artifactPaths(root, outDir).model;
429
431
  if (!inside(root, modelFile)) throw new Error("Application model must remain inside the repository");
@@ -433,7 +435,7 @@ export function prepareProductCi({ projectDir, outDir = ".autotap", modelPath =
433
435
  return { operation: "prepare-ci", ...rendered, project: readProductProject({ projectDir: root, outDir }) };
434
436
  }
435
437
 
436
- export function installProductCi({ projectDir, outDir = ".autotap", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".autotap/ci.json", replace = false, allowUnresolved = false } = {}) {
438
+ export function installProductCi({ projectDir, outDir = ".tapp", modelPath = "", actionRef = DEFAULT_ACTION_REF, defaultBranch = "main", workflowPath = ".github/workflows/tapp.yml", manifestPath = ".tapp/ci.json", replace = false, allowUnresolved = false } = {}) {
437
439
  const root = realProject(projectDir);
438
440
  const rendered = prepareProductCi({ projectDir: root, outDir, modelPath, actionRef, defaultBranch });
439
441
  if (rendered.manifest.unresolved.length && !allowUnresolved) throw new Error(`CI installation is unresolved: ${rendered.manifest.unresolved.map((item) => `${item.platform}:${item.message}`).join("; ")}`);
@@ -443,7 +445,7 @@ export function installProductCi({ projectDir, outDir = ".autotap", modelPath =
443
445
 
444
446
  export async function runProductGate({
445
447
  projectDir,
446
- outDir = ".autotap",
448
+ outDir = ".tapp",
447
449
  platform = "web",
448
450
  target = "",
449
451
  url = "",
@@ -501,7 +503,7 @@ export async function runProductGate({
501
503
  if (!inside(root, baselinePath) || !fs.existsSync(baselinePath)) throw new Error("Baseline must be an existing file inside the repository");
502
504
  args.push("--baseline", baselinePath);
503
505
  } else {
504
- const targetBaseline = baselinePathForTarget(root, selected);
506
+ const targetBaseline = existingBaselinePathForTarget(root, selected);
505
507
  if (fs.existsSync(targetBaseline)) args.push("--baseline", targetBaseline);
506
508
  }
507
509
  onProgress({ phase: "gate", text: `Running ${selected.platform}:${selected.name} release gate` });
@@ -515,7 +517,7 @@ export async function runProductGate({
515
517
  return { operation: "run-gate", passed: execution.code === 0, code: execution.code, stdout: execution.stdout, stderr: execution.stderr, selectedTarget: selected, report, reportPath, markdownPath, runDir, project: readProductProject({ projectDir: root, outDir }) };
516
518
  }
517
519
 
518
- export function createProductBaseline({ projectDir, outDir = ".autotap", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
520
+ export function createProductBaseline({ projectDir, outDir = ".tapp", reportPath, platform = "web", target = "", replace = false, baselinePath = "" } = {}) {
519
521
  const root = realProject(projectDir);
520
522
  const project = readProductProject({ projectDir: root, outDir });
521
523
  const selected = selectApplicationTarget(project.model, { platform, target });