@aarwitz/tapp 0.15.0 → 0.16.0

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.
Files changed (48) hide show
  1. package/AGENTS.md +4 -4
  2. package/Harness/OCQAHarness/AppDelegate.swift +1 -1
  3. package/Harness/OCQAHarness.xcodeproj/project.pbxproj +2 -2
  4. package/Harness/OCQAHarnessUITests/ExplorerTests.swift +2 -2
  5. package/Harness/generate-harness-xcodeproj.rb +3 -3
  6. package/README.md +17 -15
  7. package/bin/tapp.js +28 -28
  8. package/browser/app.js +2 -2
  9. package/browser/index.html +1 -1
  10. package/docs/BROWSER-PRODUCT.md +2 -2
  11. package/docs/PRODUCT-ENGINE.md +7 -6
  12. package/docs/application-model.md +20 -25
  13. package/docs/scenarios.md +7 -7
  14. package/mcp-server/src/application-model.js +32 -26
  15. package/mcp-server/src/browser-product.js +1 -1
  16. package/mcp-server/src/ci-report.js +5 -4
  17. package/mcp-server/src/ci-setup.js +16 -7
  18. package/mcp-server/src/enrich.js +1 -1
  19. package/mcp-server/src/index.js +63 -62
  20. package/mcp-server/src/maintenance-proposal.js +4 -4
  21. package/mcp-server/src/pr-selection.js +9 -8
  22. package/mcp-server/src/product-execution.js +1 -1
  23. package/mcp-server/src/product-operations.js +20 -18
  24. package/mcp-server/src/project-config.js +8 -5
  25. package/mcp-server/src/project-paths.js +32 -0
  26. package/mcp-server/src/report.js +3 -3
  27. package/mcp-server/src/task-runtime.js +14 -10
  28. package/package.json +15 -3
  29. package/scripts/ci-gate.sh +15 -10
  30. package/scripts/flow_ai_judge.py +1 -1
  31. package/scripts/flow_lib.py +1 -1
  32. package/scripts/quick-capture.sh +15 -14
  33. package/scripts/run-flow.sh +6 -6
  34. package/scripts/android-corpus-e2e.sh +0 -30
  35. package/scripts/cleanup-xcode.sh +0 -157
  36. package/scripts/corpus-apps.txt +0 -9
  37. package/scripts/corpus-sweep.sh +0 -121
  38. package/scripts/coverage-eval.sh +0 -92
  39. package/scripts/coverage_eval_parse.py +0 -95
  40. package/scripts/deploy-and-build.sh +0 -99
  41. package/scripts/mutation-recall-desktop.sh +0 -186
  42. package/scripts/mutation-recall.sh +0 -121
  43. package/scripts/mutation_lib.py +0 -128
  44. package/scripts/mutation_operators.py +0 -144
  45. package/scripts/validation-matrix.sh +0 -146
  46. package/scripts/vision-fp-eval.sh +0 -206
  47. package/scripts/vision_escalation_responder.py +0 -147
  48. package/scripts/vision_fp_probe.py +0 -221
package/docs/scenarios.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Multi-actor Scenarios
2
2
 
3
- A Scenario is Tapp's low-level deterministic multi-actor execution format in `.autotap/scenarios/*.yml`. It uses the same semantic actions, polling assertions, timeouts, evidence markers, and merge policy as a Flow, but adds isolated named actors, shared variables, and explicit lifecycle steps. The customer-facing business authoring layer is a TypeScript [release contract](release-contracts.md), which composes reusable Tasks and compiles to this runtime instead of duplicating UI steps.
3
+ A Scenario is Tapp's low-level deterministic multi-actor execution format in `.tapp/scenarios/*.yml`. It uses the same semantic actions, polling assertions, timeouts, evidence markers, and merge policy as a Flow, but adds isolated named actors, shared variables, and explicit lifecycle steps. The customer-facing business authoring layer is a TypeScript [release contract](release-contracts.md), which composes reusable Tasks and compiles to this runtime instead of duplicating UI steps.
4
4
 
5
5
  Ordinary replay is keyless. AI may propose a Scenario during authoring, but no model, API key, or coding agent participates when CI executes it.
6
6
 
@@ -61,13 +61,13 @@ teardown:
61
61
  ## Run it
62
62
 
63
63
  ```bash
64
- tapp scenario validate .autotap/scenarios/social-system.yml
64
+ tapp scenario validate .tapp/scenarios/social-system.yml
65
65
  ALICE_EMAIL=alice@example.test ALICE_PASSWORD=demo \
66
66
  BOB_EMAIL=bob@example.test BOB_PASSWORD=demo \
67
- tapp scenario run .autotap/scenarios/social-system.yml
67
+ tapp scenario run .tapp/scenarios/social-system.yml
68
68
 
69
69
  tapp ci --platform web --url http://127.0.0.1:4180 \
70
- --scenarios '.autotap/scenarios/*.yml' \
70
+ --scenarios '.tapp/scenarios/*.yml' \
71
71
  --json-out tapp-report.json --md-out tapp-report.md
72
72
  ```
73
73
 
@@ -78,16 +78,16 @@ GitHub Action:
78
78
  with:
79
79
  platform: web
80
80
  url: http://127.0.0.1:4180
81
- scenarios: .autotap/scenarios/*.yml
81
+ scenarios: .tapp/scenarios/*.yml
82
82
  ```
83
83
 
84
84
  MCP clients call `tapp_scenario_run` with `scenarioPath`, or an inline reviewed Scenario. The structured result identifies `kind: scenario`, actual executed/total steps, and actor-tagged steps.
85
85
 
86
86
  ## Verified fixture and boundaries
87
87
 
88
- `SocialDemo/.autotap/contracts/social-system.contract.ts` is the reference system guarantee; the Scenario remains its low-level execution proof and backwards-compatible escape hatch. On 2026-08-04 the contract passed 41/41 compiled steps using two isolated contexts against one delayed shared backend. With `SOCIAL_DEMO_FAULT=hide-cross-actor-posts`, the unchanged contract failed for Bob at 19/41 and the portable merge gate exited non-zero specifically because one release contract failed.
88
+ `SocialDemo/.tapp/contracts/social-system.contract.ts` is the reference system guarantee; the Scenario remains its low-level execution proof and backwards-compatible escape hatch. On 2026-08-04 the contract passed 41/41 compiled steps using two isolated contexts against one delayed shared backend. With `SOCIAL_DEMO_FAULT=hide-cross-actor-posts`, the unchanged contract failed for Bob at 19/41 and the portable merge gate exited non-zero specifically because one release contract failed.
89
89
 
90
- The fixture's `.autotap/project.json` is the central actor contract. It records Alice and Bob's
90
+ The fixture's `.tapp/project.json` is the central actor contract. It records Alice and Bob's
91
91
  roles, isolated sessions, seeded provisioning, reset lifecycle, and four environment-variable
92
92
  names. The release contract and Scenario consume those names; neither stores the public fixture
93
93
  values. Customer values belong in the local environment or CI secret store.
@@ -8,6 +8,7 @@ import { credentialBindingsFromValue, readProjectConfig } from "./project-config
8
8
  import { applyReleaseContractCoverage, compileReleaseContract, loadReleaseContractFile, validateReleaseContractAgainstUiMap } from "./release-contract.js";
9
9
  import { applyTaskCoverage, loadTaskFile, validateTaskAgainstUiMap } from "./task-runtime.js";
10
10
  import { semanticUiKey } from "./ui-map.js";
11
+ import { isProjectArtifactDirectory, projectArtifactDirectory } from "./project-paths.js";
11
12
 
12
13
  const SKIP = new Set([".git", ".build", ".gradle", ".next", ".swiftpm", "Pods", "Carthage", "DerivedData", "build", "dist", "node_modules", "vendor"]);
13
14
 
@@ -25,7 +26,7 @@ function walk(root, maxDepth = 4) {
25
26
  let entries;
26
27
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
27
28
  for (const entry of entries) {
28
- if (SKIP.has(entry.name) || (entry.name.startsWith(".") && entry.name !== ".autotap")) continue;
29
+ if (SKIP.has(entry.name) || (entry.name.startsWith(".") && !isProjectArtifactDirectory(entry.name))) continue;
29
30
  const absolute = path.join(dir, entry.name);
30
31
  if (entry.isDirectory()) {
31
32
  directories.push(absolute);
@@ -124,7 +125,8 @@ function applyRuntimeTargetValidation(root, targets, validation) {
124
125
  }
125
126
 
126
127
  function persistedTargetValidations(root, outDir) {
127
- const artifactDir = path.resolve(root, String(outDir || ".autotap"));
128
+ const requested = String(outDir || ".tapp");
129
+ const artifactDir = path.resolve(root, projectArtifactDirectory(root, requested));
128
130
  const relativeArtifactDir = path.relative(root, artifactDir);
129
131
  if (path.isAbsolute(relativeArtifactDir) || relativeArtifactDir === ".." || relativeArtifactDir.startsWith(`..${path.sep}`)) return [];
130
132
  const prior = readJson(path.join(artifactDir, "application-model.json"));
@@ -299,10 +301,11 @@ function uiMapTargetsTarget(map, target, targets) {
299
301
  }
300
302
 
301
303
  function loadTargetUiMaps(root, targets) {
302
- const rootMap = loadUiMapAt(root, path.join(".autotap", "ui-map.json"));
304
+ const rootMap = loadUiMapAt(root, path.join(projectArtifactDirectory(root), "ui-map.json"));
303
305
  const records = targets.map((target) => {
304
306
  const scope = targetArtifactScope(target);
305
- const expectedPath = posix(path.join(scope === "." ? "" : scope, ".autotap", "ui-map.json"));
307
+ const scopeRoot = path.join(root, scope === "." ? "" : scope);
308
+ const expectedPath = posix(path.join(scope === "." ? "" : scope, projectArtifactDirectory(scopeRoot), "ui-map.json"));
306
309
  let loaded = expectedPath === rootMap.summary.path ? rootMap : loadUiMapAt(root, expectedPath);
307
310
  if (!loaded.map && rootMap.map && (targets.length === 1 || uiMapTargetsTarget(rootMap.map, target, targets))) loaded = rootMap;
308
311
  return {
@@ -328,7 +331,7 @@ function loadTargetUiMaps(root, targets) {
328
331
  const prefixCoverage = unique.length > 1;
329
332
  const coverageValues = (field) => unique.flatMap((record) => (record.summary[field] || []).map((id) => prefixCoverage ? `${record.summary.targetId}:${id}` : id));
330
333
  const summary = {
331
- path: unique.length === 1 ? unique[0].summary.path : ".autotap/ui-map.json",
334
+ path: unique.length === 1 ? unique[0].summary.path : ".tapp/ui-map.json",
332
335
  paths: unique.map((record) => record.summary.path).sort(),
333
336
  status: allObserved ? "observed" : someObserved ? "partial" : someInconclusive ? "inconclusive" : "missing",
334
337
  nodeCount: unique.reduce((total, record) => total + record.summary.nodeCount, 0),
@@ -348,14 +351,14 @@ function loadTargetUiMaps(root, targets) {
348
351
 
349
352
  function artifactScope(root, file) {
350
353
  const parts = relative(root, file).split("/");
351
- const index = parts.indexOf(".autotap");
354
+ const index = parts.findIndex(isProjectArtifactDirectory);
352
355
  return index > 0 ? parts.slice(0, index).join("/") : ".";
353
356
  }
354
357
 
355
358
  function artifactFiles(root, inventory, kind, pattern) {
356
359
  return inventory.files.filter((file) => {
357
360
  const parts = relative(root, file).split("/");
358
- const index = parts.indexOf(".autotap");
361
+ const index = parts.findIndex(isProjectArtifactDirectory);
359
362
  return index >= 0 && parts[index + 1] === kind && pattern.test(path.basename(file));
360
363
  }).sort();
361
364
  }
@@ -421,7 +424,7 @@ function applicationName(root, targets) {
421
424
  return pkg?.name || (targets.length === 1 ? targets[0].name : path.basename(root));
422
425
  }
423
426
 
424
- export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, outDir = ".autotap" } = {}) {
427
+ export async function inspectApplicationRepository({ projectDir, ownedUrl = "", platform = "", targetValidation = null, outDir = ".tapp" } = {}) {
425
428
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
426
429
  const inventory = walk(root);
427
430
  let targets = [
@@ -550,7 +553,7 @@ export async function inspectApplicationRepository({ projectDir, ownedUrl = "",
550
553
  requirements.push({
551
554
  id: targets.length <= 1 ? "ui-map" : `${target.id}:ui-map`, severity: "blocking", status: summary.status === "inconclusive" ? "inconclusive" : "missing",
552
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.",
553
- 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 .autotap/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 tapp init --explore so real exploration evidence is merged into .tapp/ui-map.json.",
554
557
  });
555
558
  }
556
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." });
@@ -925,7 +928,7 @@ function invalidateGeneratedTaskFiles(root, plan) {
925
928
  for (const record of plan.generation?.generatedTasks || []) {
926
929
  if (!record.path) continue;
927
930
  const absolute = path.resolve(root, record.path);
928
- const proposalRoot = path.join(root, ".autotap", "proposals", "tasks");
931
+ const proposalRoot = path.join(root, projectArtifactDirectory(root), "proposals", "tasks");
929
932
  if (!isInsideRoot(proposalRoot, absolute) || !fs.existsSync(absolute)) continue;
930
933
  const task = readJson(absolute);
931
934
  if (!task || task.generation?.origin !== "deterministic-ui-map") continue;
@@ -937,8 +940,8 @@ function invalidateGeneratedTaskFiles(root, plan) {
937
940
  }
938
941
  }
939
942
 
940
- export function writeInitArtifacts({ root, model, plan, outDir = ".autotap", refresh = false, invalidateValidation = false } = {}) {
941
- const directory = path.resolve(root, outDir);
943
+ export function writeInitArtifacts({ root, model, plan, outDir = ".tapp", refresh = false, invalidateValidation = false } = {}) {
944
+ const directory = path.resolve(root, projectArtifactDirectory(root, outDir));
942
945
  const modelPath = path.join(directory, "application-model.json");
943
946
  const planPath = path.join(directory, "release-plan.json");
944
947
  if (!refresh && (fs.existsSync(modelPath) || fs.existsSync(planPath))) throw new Error(`Init artifacts already exist under ${relative(root, directory)}; inspect them or rerun with --refresh to preserve reviewed decisions while updating evidence`);
@@ -1069,7 +1072,7 @@ function generatedTaskName(node, entryOnly, occupied, identity) {
1069
1072
  return `${base}${crypto.createHash("sha256").update(identity).digest("hex").slice(0, 6)}`;
1070
1073
  }
1071
1074
 
1072
- function prepareMapBackedItem(item, map, existingTasks, taskDrafts, { targetId = "", mapPath = ".autotap/ui-map.json" } = {}) {
1075
+ function prepareMapBackedItem(item, map, existingTasks, taskDrafts, { targetId = "", mapPath = ".tapp/ui-map.json" } = {}) {
1073
1076
  const ground = (item.groundedBy || []).find((entry) => entry.type === "ui-map-node");
1074
1077
  if (!ground) throw new Error("Approved UI-only proposal is not grounded by a UI Map node");
1075
1078
  const nodes = new Map((map.nodes || []).map((node) => [node.id, node]));
@@ -1168,7 +1171,7 @@ function writeGeneratedTaskDrafts(root, taskDrafts) {
1168
1171
  for (const draft of [...taskDrafts.values()].sort((a, b) => a.name.localeCompare(b.name))) {
1169
1172
  const scopeRoot = path.resolve(root, draft.scope === "." || !draft.scope ? "" : draft.scope);
1170
1173
  if (!isInsideRoot(root, scopeRoot)) throw new Error(`Generated Task scope escapes repository: ${draft.scope}`);
1171
- const output = path.join(scopeRoot, ".autotap", "proposals", "tasks", `${kebab(draft.name)}.task.json`);
1174
+ const output = path.join(scopeRoot, projectArtifactDirectory(scopeRoot), "proposals", "tasks", `${kebab(draft.name)}.task.json`);
1172
1175
  const definition = {
1173
1176
  kind: "task", version: 1, name: draft.name, description: draft.description,
1174
1177
  ...(Object.keys(draft.inputs || {}).length ? { inputs: draft.inputs } : {}),
@@ -1189,7 +1192,7 @@ function writeGeneratedTaskDrafts(root, taskDrafts) {
1189
1192
  }
1190
1193
  try {
1191
1194
  const task = loadTaskFile(output);
1192
- const mapPath = path.resolve(root, draft.mapPath || ".autotap/ui-map.json");
1195
+ const mapPath = path.resolve(root, draft.mapPath || ".tapp/ui-map.json");
1193
1196
  if (!isInsideRoot(root, mapPath)) throw new Error(`Generated Task UI Map escapes repository: ${draft.mapPath}`);
1194
1197
  const map = readJson(mapPath);
1195
1198
  if (!map || map.schemaVersion !== 1) throw new Error(`Generated Task UI Map is missing or invalid: ${draft.mapPath}`);
@@ -1304,7 +1307,7 @@ export async function generateApprovedContractProposals(plan, { projectDir } = {
1304
1307
  let prepared = item;
1305
1308
  if (item.origin === "deterministic-ui-map-proposal") {
1306
1309
  const grounding = (item.groundedBy || []).find((entry) => entry.type === "ui-map-node");
1307
- const mapPath = grounding?.mapPath || ".autotap/ui-map.json";
1310
+ const mapPath = grounding?.mapPath || ".tapp/ui-map.json";
1308
1311
  const absoluteMapPath = path.resolve(root, mapPath);
1309
1312
  const itemMap = isInsideRoot(root, absoluteMapPath) ? readJson(absoluteMapPath) : null;
1310
1313
  if (!itemMap || itemMap.schemaVersion !== 1) {
@@ -1329,7 +1332,7 @@ export async function generateApprovedContractProposals(plan, { projectDir } = {
1329
1332
  if (!prepared) continue;
1330
1333
  const scopeRoot = path.resolve(root, item.scope === "." || !item.scope ? "" : item.scope);
1331
1334
  if (scopeRoot !== root && !scopeRoot.startsWith(root + path.sep)) throw new Error(`Plan scope escapes repository: ${item.scope}`);
1332
- const output = path.join(scopeRoot, ".autotap", "proposals", "contracts", `${kebab(item.name)}.contract.ts`);
1335
+ const output = path.join(scopeRoot, projectArtifactDirectory(scopeRoot), "proposals", "contracts", `${kebab(item.name)}.contract.ts`);
1333
1336
  const source = draftContractSource(prepared, projectConfiguration.config || {});
1334
1337
  if (fs.existsSync(output) && fs.readFileSync(output, "utf8") !== source) throw new Error(`Draft contract already exists with different content and was not overwritten: ${relative(root, output)}`);
1335
1338
  const created = !fs.existsSync(output);
@@ -1412,13 +1415,13 @@ export function recordContractProposalValidation(plan, { id = "", name = "", pla
1412
1415
  export function recordGeneratedTaskProposalValidation({ projectDir, item, platform, evidence = "", detail = "" } = {}) {
1413
1416
  if (!["ios", "android", "web"].includes(platform)) throw new Error("platform must be ios|android|web");
1414
1417
  const root = fs.realpathSync(path.resolve(projectDir || process.cwd()));
1415
- const proposalMarker = `${path.sep}.autotap${path.sep}proposals${path.sep}tasks${path.sep}`;
1416
- const reviewedMarker = `${path.sep}.autotap${path.sep}tasks${path.sep}`;
1418
+ const proposalMarkers = [".tapp", ".autotap"].map((directory) => `${path.sep}${directory}${path.sep}proposals${path.sep}tasks${path.sep}`);
1419
+ const reviewedMarkers = [".tapp", ".autotap"].map((directory) => `${path.sep}${directory}${path.sep}tasks${path.sep}`);
1417
1420
  const updated = [];
1418
1421
  for (const taskPath of item?.generation?.taskPaths || []) {
1419
1422
  const absolute = path.resolve(root, taskPath);
1420
- const proposed = isInsideRoot(root, absolute) && absolute.includes(proposalMarker);
1421
- const reviewed = isInsideRoot(root, absolute) && absolute.includes(reviewedMarker) && !absolute.includes(proposalMarker);
1423
+ const proposed = isInsideRoot(root, absolute) && proposalMarkers.some((marker) => absolute.includes(marker));
1424
+ const reviewed = isInsideRoot(root, absolute) && reviewedMarkers.some((marker) => absolute.includes(marker)) && !proposed;
1422
1425
  if ((!proposed && !reviewed) || !fs.existsSync(absolute)) throw new Error(`Generated Task is missing or outside Tapp Task directories: ${taskPath}`);
1423
1426
  const task = readJson(absolute);
1424
1427
  if (!task || task.kind !== "task" || task.generation?.origin !== "deterministic-ui-map") throw new Error(`Generated Task draft has invalid provenance: ${taskPath}`);
@@ -1458,10 +1461,13 @@ export function mergeGeneratedTaskProposalValidation(plan, updates = []) {
1458
1461
  }
1459
1462
 
1460
1463
  function promotedDestination(root, source, kind) {
1461
- const marker = `${path.sep}.autotap${path.sep}proposals${path.sep}${kind}${path.sep}`;
1464
+ const marker = [".tapp", ".autotap"]
1465
+ .map((directory) => `${path.sep}${directory}${path.sep}proposals${path.sep}${kind}${path.sep}`)
1466
+ .find((candidate) => source.includes(candidate));
1467
+ if (!marker) throw new Error(`Proposal ${kind.slice(0, -1)} is outside .tapp/proposals/${kind}: ${relative(root, source)}`);
1462
1468
  const index = source.indexOf(marker);
1463
- if (index < 0) throw new Error(`Proposal ${kind.slice(0, -1)} is outside .autotap/proposals/${kind}: ${relative(root, source)}`);
1464
- const destination = `${source.slice(0, index)}${path.sep}.autotap${path.sep}${kind}${path.sep}${source.slice(index + marker.length)}`;
1469
+ const directory = marker.split(path.sep).filter(Boolean)[0];
1470
+ const destination = `${source.slice(0, index)}${path.sep}${directory}${path.sep}${kind}${path.sep}${source.slice(index + marker.length)}`;
1465
1471
  if (!isInsideRoot(root, destination)) throw new Error(`Promotion destination escapes repository: ${destination}`);
1466
1472
  return destination;
1467
1473
  }
@@ -1480,7 +1486,7 @@ export async function promoteValidatedProposals(plan, { projectDir, ids = [] } =
1480
1486
 
1481
1487
  const mapCache = new Map();
1482
1488
  const mapForItem = (item) => {
1483
- const relativeMapPath = item.generation?.mapPath || (item.groundedBy || []).find((entry) => entry.type === "ui-map-node")?.mapPath || ".autotap/ui-map.json";
1489
+ const relativeMapPath = item.generation?.mapPath || (item.groundedBy || []).find((entry) => entry.type === "ui-map-node")?.mapPath || ".tapp/ui-map.json";
1484
1490
  const absolute = path.resolve(root, relativeMapPath);
1485
1491
  if (!isInsideRoot(root, absolute)) throw new Error(`UI Map for '${item.name}' escapes the repository: ${relativeMapPath}`);
1486
1492
  if (!mapCache.has(absolute)) {
@@ -1499,7 +1505,7 @@ export async function promoteValidatedProposals(plan, { projectDir, ids = [] } =
1499
1505
  for (const taskPath of item.generation.taskPaths || []) {
1500
1506
  const source = path.resolve(root, taskPath);
1501
1507
  if (!fs.existsSync(source)) throw new Error(`Generated Task is missing: ${taskPath}`);
1502
- if (!String(source).includes(`${path.sep}.autotap${path.sep}proposals${path.sep}tasks${path.sep}`)) continue;
1508
+ if (![".tapp", ".autotap"].some((directory) => String(source).includes(`${path.sep}${directory}${path.sep}proposals${path.sep}tasks${path.sep}`))) continue;
1503
1509
  const destination = promotedDestination(root, source, "tasks");
1504
1510
  moves.set(source, destination);
1505
1511
  let task = taskRecords.get(source);
@@ -109,7 +109,7 @@ function assetPath(pathname) {
109
109
  }
110
110
 
111
111
  function captureRoot() {
112
- return path.join(process.env.AUTOTAP_HOME || process.env.TAPP_HOME || path.join(os.homedir(), ".tapp"), "captures");
112
+ return path.join(process.env.TAPP_HOME || process.env.AUTOTAP_HOME || path.join(os.homedir(), ".tapp"), "captures");
113
113
  }
114
114
 
115
115
  function capturePath(captureId, relative = "report.html") {
@@ -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");
@@ -40,7 +40,7 @@ export async function enrichFindings(findings, { backend, callModel, screens = [
40
40
  `Title: ${f.title}`;
41
41
  try {
42
42
  const res = await Promise.race([
43
- callModel(backend, { system, userText, model: process.env.AUTOTAP_FINDING_MODEL || "claude-haiku-4-5-20251001", maxTokens: 300 }),
43
+ callModel(backend, { system, userText, model: process.env.TAPP_FINDING_MODEL || process.env.AUTOTAP_FINDING_MODEL || "claude-haiku-4-5-20251001", maxTokens: 300 }),
44
44
  new Promise((r) => setTimeout(() => r({ error: "timeout" }), TIMEOUT_MS)),
45
45
  ]);
46
46
  const parsed = res && !res.error ? parseEnrichment(res.text) : null;