transit branch T-1628/bugfix-test-quick-workspace-local-cache-regression commits 3 files 5 touched lines +292 / -131 PR #223 · OPEN · CLEAN

Pre-push review: T-1628 cache isolation

Read-only final review of PR #223. Exact head 1523d1fa9f6429cd8c6f5efdf6bf6f26cedcf0d4 is reviewed against ddfcc7acf164ba0942a0bf04e98d8cd576f773c0.

At a glance

  • Exact refs: PR #223 is OPEN, base ddfcc7acf164ba0942a0bf04e98d8cd576f773c0, head 1523d1fa9f6429cd8c6f5efdf6bf6f26cedcf0d4; the local branch and upstream both resolve to that exact head.
  • External state: GitHub reports merge state CLEAN; claude-review succeeded on the exact head (run); GraphQL reports 0 review threads and no formal reviews.
  • Validation: the restricted-HOME iOS build left Clang, SwiftPM manifest, and package-resolution outputs under DerivedData; no protected-home Clang or SwiftPM cache was created; make clean removed DerivedData.
  • Review outcome: code-reuse, quality, efficiency, documentation, and regression-coverage passes found no actionable defects. No tracked source was changed during this review.

Verdict

Ready to push

The cache-isolation correction, package-resolution safeguard, and macOS GNU Make pipefail fix are internally consistent and independently validated. The restricted-HOME iOS build and cleanup, Makefile guard, its negative controls, hermetic install dry run, pipe-failure smoke test, and strict lint all passed. make test-quick is blocked only by the host’s missing Mac Development signing certificate, after cache/package setup; it is not a defect in this change.

Commits

Three-level explanation

What changed

Building the app used to leave some temporary files in a person’s home folder. That fails in sandboxes where the home folder is locked. The Makefile now sends those files into the project’s DerivedData folder instead.

Why it matters

Developers and automated agents can build without permission errors, and make clean removes the temporary files in one place. The build also no longer risks reporting success when its formatter hides an Xcode failure.

Key concepts

DerivedData is the app’s local build workspace. A cache is a reusable temporary file. pipefail makes a pipeline report a failure from the real build command rather than only the final display tool.

Completeness assessment

All requested cache controls, cleanup behavior, and failure propagation are implemented and guarded. The only unavailable confirmation is a completed macOS test run on this host because its signing credential is absent.

Changes overview

Makefile passes CLANG_MODULE_CACHE_PATH as an xcodebuild build setting, exports SWIFTPM_MODULECACHE_OVERRIDE for manifest compilation, retains local XDG/TMP paths, and intentionally omits package-cache flags that prevent normal resolution. It explicitly prefixes every xcodebuild recipe with Bash pipefail.

Implementation approach

The shared XCODEBUILD_CACHE_FLAGS and XCODEBUILD_ENV abstractions keep every source-building target consistent. The shell guard obtains make -n expansions and asserts exact absolute workspace paths, setting position, omitted flags, cache-directory creation, cleanup, and the install-target override.

Trade-offs

The fix does not redirect the whole HOME, avoiding unrelated credentials and tool configuration. It keeps Xcode responsible for SourcePackages under -derivedDataPath, rather than forcing incompatible checkout/cache subdirectory flags.

Completeness assessment

The reviewed paths are covered by direct guard checks and a real restricted-HOME iOS build. The remaining test-run limitation is environmental, documented, and reproduced as missing signing material rather than a cache escape.

Technical deep dive

The prior environment-only Clang setting did not populate Xcode’s CLANG_MODULE_CACHE_PATH build setting. The branch moves it into XCODEBUILD_CACHE_FLAGS, while SwiftPM’s manifest compiler receives the separate SWIFTPM_MODULECACHE_OVERRIDE environment interface. The guard’s xcodebuild-argument extraction catches an env-only regression, and its exact path assertions prevent a silent return to user-global locations.

Architecture impact

Cache policy remains centralized in Make variables rather than copied across recipes. Adding a source-building target has a clear contract: depend on prepare-cache-dirs and use both shared controls. Inline set -o pipefail; is deliberately retained alongside .SHELLFLAGS: GNU Make 3.81 on macOS ignores the latter, while newer Make implementations still benefit from it.

Edge cases and monitoring

DEVICE_ID is recursively expanded even for make -n; the guard’s install-only command-line override avoids device enumeration, and a fake-xcrun test proves it. The SwiftPM override is toolchain-sensitive; the real restricted-HOME build confirms current behavior, so future Xcode/SwiftPM upgrades should rerun this guard and build validation.

Completeness assessment

No implementation/spec divergence or unresolved review thread remains. The change is ready; host signing remains the sole blocker to an end-to-end macOS unit-test execution.

Important changes — detailed

Makefile: localize Xcode and SwiftPM caches

Makefile

Why it matters. Prevents restricted-HOME builds from writing Clang modules and SwiftPM manifest diagnostics to user-global caches.

What to look at. Makefile:80-112

Takeaway. Treat Xcode build settings and SwiftPM environment overrides as different configuration interfaces.
Rationale. The report records that the environment-only Clang variable did not affect Xcode’s build setting, whereas the command-line build setting does.

Makefile: make pipeline failures observable on GNU Make 3.81

Makefile

Why it matters. A failing xcodebuild can no longer be hidden by a successful xcbeautify process on macOS’s bundled Make.

What to look at. Makefile:19-21, 120-254

Takeaway. When compatibility tooling ignores global shell flags, put required pipeline semantics on the recipe line itself.
Rationale. macOS GNU Make 3.81 ignores .SHELLFLAGS; each xcodebuild recipe therefore starts an explicit Bash pipefail scope.

Regression guard: assert behavior and intentional omissions

tests/makefile/test_workspace_local_caches.sh

Why it matters. Turns the previous stale guard into coverage for exact local paths, build-setting placement, hermetic dry runs, and package-resolution safety.

What to look at. tests/makefile/test_workspace_local_caches.sh:20-105

Takeaway. A Makefile dry-run guard can prevent configuration regressions without invoking an expensive platform build.
Rationale. The obsolete package flags are intentionally absent because their subdirectory values disable Xcode’s normal package-resolution path.

Documentation: preserve the non-obvious tool-boundary rationale

docs/agent-notes/project-structure.md

Why it matters. Future changes have an explicit explanation of which controls are build settings, environment variables, and deliberately omitted flags.

What to look at. docs/agent-notes/project-structure.md:77-100; specs/bugfixes/test-quick-workspace-local-cache-regression/report.md:1-153

Takeaway. Document tool-specific cache controls next to the regression test that enforces them.
Rationale. The bugfix report traces the root cause, rejected alternatives, and validation limits.

Key decisions

Use an Xcode build setting for the Clang module cache.

CLANG_MODULE_CACHE_PATH is passed after xcodebuild, not merely exported. This is the interface Xcode consumes for compiler module-cache configuration.

Use a SwiftPM override for manifest compilation.

SWIFTPM_MODULECACHE_OVERRIDE localizes manifest modules and diagnostics before a project target compiles, while XDG/TMP cover complementary process cache and temporary paths.

Keep package-cache flags absent.

-derivedDataPath leaves Xcode to manage SourcePackages within the workspace. The explicitly omitted checkout/cache flags prevent normal package resolution when pointed at subdirectories.

Use inline pipefail as the portability boundary.

The project keeps .SHELLFLAGS for newer GNU Make but puts set -o pipefail; directly in each xcodebuild recipe to support macOS GNU Make 3.81.

Per-file diffs

Click to expand.

CHANGELOG.md Modified +1 / -0
diff --git a/CHANGELOG.md b/CHANGELOG.mdindex 5c6a897..eb6598a 100644--- a/CHANGELOG.md+++ b/CHANGELOG.md@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).  - T-1803: `UpdateStatusIntent` now includes a missing requested `displayId` in its `TASK_NOT_FOUND` hint (`No task with displayId N`), while missing UUIDs retain the existing generic lookup hint and malformed identifiers, duplicate IDs, status validation, and atomic mutation behavior remain unchanged. ### Fixed+- T-1628: `make test-quick` and every relevant source-building Makefile target now keep Clang module caches and SwiftPM manifest modules/diagnostics workspace-local. `CLANG_MODULE_CACHE_PATH` is passed as an Xcode build setting, `SWIFTPM_MODULECACHE_OVERRIDE` is exported for manifest compilation, and the cache guard now asserts these supported controls while explicitly preserving the intentional omission of `-clonedSourcePackagesDirPath` and `-packageCachePath` so package resolution continues to work. Xcode recipes also explicitly enable `pipefail`, preventing `xcbeautify` from masking build failures on macOS's bundled GNU Make 3.81. - T-1613: MCP `query_tasks` now returns the exact tool error `Failed to fetch comments: <error>` when comment serialization cannot read storage, rather than reporting a successful task with `comments: []`. The detailed display-ID and task-list paths share this throwing serialization boundary; genuine empty comment collections retain their successful `comments: []` response. `update_task_status` continues serializing the `Comment` returned by its atomic mutation directly (T-1823), so it performs no post-commit comment fetch that could prompt a retry or duplicate a persisted comment. Deterministic MCP regressions cover both query errors, legitimate empty comments, and zero status-response fetches. - T-1620: `GenerateReportIntent` now returns the established `INTERNAL_ERROR` JSON envelope with a source-specific stable hint when terminal task or milestone fetches fail, instead of false empty-report Markdown. Successful empty reports retain their existing Markdown and date-range formatting; deterministic regressions cover both failures and the valid-empty response. - T-2037: Add Task observes the selected project's milestones and clears a selection that leaves the open picker options. It also revalidates an optional milestone at the final pre-insertion boundary using both live and fresh committed SwiftData state. A milestone closed in another window/context while display-ID allocation awaits now rejects with a user-facing error before any task insertion; nil milestones, same-project validation, one-save task/milestone creation, and dashboard terminal-filter behavior remain unchanged. Deterministic two-context regressions cover both Done and Abandoned transitions and verify no pending or committed task survives.
Makefile Modified +53 / -15
diff --git a/Makefile b/Makefileindex 892dff6..b8ba0e7 100644--- a/Makefile+++ b/Makefile@@ -16,6 +16,10 @@ else PIPE_PRETTY = endif +# macOS ships GNU Make 3.81, which ignores .SHELLFLAGS. Enable pipefail in+# each recipe that may pipe xcodebuild through xcbeautify so failures propagate.+PIPEFAIL = set -o pipefail;+ # Default target .PHONY: help help:@@ -75,36 +79,41 @@ DERIVED_DATA = ./DerivedData # Workspace-local cache locations. Xcode and its subprocesses (SwiftPM, Clang) # otherwise scatter caches across ~/Library/Caches and ~/.cache, which fail in # sandboxed/dev environments. Keep everything under DerivedData so a single-# `make clean` is enough. See T-1241.-SPM_CACHE        = $(DERIVED_DATA)/SourcePackages/cache-SPM_CLONED       = $(DERIVED_DATA)/SourcePackages/checkouts-WORKSPACE_CACHE  = $(DERIVED_DATA)/Caches-WORKSPACE_TMP    = $(DERIVED_DATA)/tmp+# `make clean` is enough. See T-1241 and T-1628.+WORKSPACE_CACHE = $(DERIVED_DATA)/Caches+WORKSPACE_TMP = $(DERIVED_DATA)/tmp+SPM_MANIFEST_MODULE_CACHE = $(WORKSPACE_CACHE)/org.swift.swiftpm CLANG_MODULE_CACHE = $(DERIVED_DATA)/ModuleCache.noindex  # -clonedSourcePackagesDirPath and -packageCachePath are intentionally omitted: # they expect the SourcePackages parent dir (not its checkouts/cache subdirs), # and supplying both silently disables xcodebuild's package resolve step. With-# only -derivedDataPath set, xcodebuild places SPM artifacts at-# $(DERIVED_DATA)/SourcePackages, which already keeps everything workspace-local.+# only -derivedDataPath set, xcodebuild places SourcePackages under+# $(DERIVED_DATA), which already keeps package checkouts and repository data+# workspace-local.+#+# CLANG_MODULE_CACHE_PATH must be an xcodebuild build setting, not just an+# environment variable, so the compiler receives the module-cache path. XCODEBUILD_CACHE_FLAGS = \-	-derivedDataPath $(DERIVED_DATA)+	-derivedDataPath $(DERIVED_DATA) \+	CLANG_MODULE_CACHE_PATH=$(abspath $(CLANG_MODULE_CACHE)) -# Exported before every xcodebuild call so SwiftPM resolution, Clang module-# cache fallbacks ($XDG_CACHE_HOME/clang/ModuleCache), and compiler temp-# diagnostics (.dia) all stay inside the workspace.+# Exported before every xcodebuild call so XDG cache fallbacks, compiler temp+# files, and SwiftPM manifest compilation (including *.dia diagnostics) stay+# inside the workspace. SWIFTPM_MODULECACHE_OVERRIDE is recognized by SwiftPM;+# Xcode's own Clang cache is configured above as a build setting. XCODEBUILD_ENV = \ 	XDG_CACHE_HOME=$(abspath $(WORKSPACE_CACHE)) \ 	TMPDIR=$(abspath $(WORKSPACE_TMP)) \-	CLANG_MODULE_CACHE_PATH=$(abspath $(CLANG_MODULE_CACHE))+	SWIFTPM_MODULECACHE_OVERRIDE=$(abspath $(SPM_MANIFEST_MODULE_CACHE))  .PHONY: prepare-cache-dirs prepare-cache-dirs:-	@mkdir -p $(SPM_CACHE) $(SPM_CLONED) $(WORKSPACE_CACHE) $(WORKSPACE_TMP) $(CLANG_MODULE_CACHE)+	@mkdir -p $(WORKSPACE_CACHE) $(WORKSPACE_TMP) $(SPM_MANIFEST_MODULE_CACHE) $(CLANG_MODULE_CACHE)  .PHONY: build-ios build-ios: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild build \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild build \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'platform=iOS Simulator,name=iPhone 17' \@@ -114,7 +123,7 @@ build-ios: prepare-cache-dirs  .PHONY: build-macos build-macos: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild build \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild build \ 		-allowProvisioningUpdates \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \@@ -129,7 +138,7 @@ build: build-ios build-macos # Testing .PHONY: test-quick test-quick: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild test \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild test \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'platform=macOS' \@@ -140,7 +149,7 @@ test-quick: prepare-cache-dirs  .PHONY: test test: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild test \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild test \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'platform=iOS Simulator,name=iPhone 17' \@@ -152,7 +161,7 @@ test: prepare-cache-dirs  .PHONY: test-ui test-ui: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild test \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild test \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'platform=iOS Simulator,name=iPhone 17' \@@ -177,7 +186,7 @@ install: prepare-cache-dirs 		exit 1; \ 	fi 	@echo "Building $(CONFIG) for device $(DEVICE_ID)..."-	$(XCODEBUILD_ENV) xcodebuild build \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild build \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'id=$(DEVICE_ID)' \@@ -218,7 +227,7 @@ EXPORT_PATH = ./build/export  .PHONY: archive archive: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild archive \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild archive \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		-destination 'generic/platform=iOS' \@@ -234,7 +243,7 @@ archive: prepare-cache-dirs #     -exportOptionsPlist ExportOptions.plist -exportPath ./build/export -allowProvisioningUpdates .PHONY: upload upload: archive-	$(XCODEBUILD_ENV) xcodebuild -exportArchive \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild -exportArchive \ 		-archivePath $(ARCHIVE_PATH) \ 		-exportOptionsPlist ExportOptions.plist \ 		-exportPath $(EXPORT_PATH) \@@ -244,7 +253,7 @@ upload: archive # Cleaning .PHONY: clean clean: prepare-cache-dirs-	$(XCODEBUILD_ENV) xcodebuild clean \+	$(PIPEFAIL) $(XCODEBUILD_ENV) xcodebuild clean \ 		-project $(PROJECT) \ 		-scheme $(SCHEME) \ 		$(XCODEBUILD_CACHE_FLAGS)
docs/agent-notes/project-structure.md Modified +28 / -24
diff --git a/docs/agent-notes/project-structure.md b/docs/agent-notes/project-structure.mdindex f00b95a..8d4bba3 100644--- a/docs/agent-notes/project-structure.md+++ b/docs/agent-notes/project-structure.md@@ -77,20 +77,24 @@ Every `xcodebuild` invocation in the Makefile redirects its caches into  Two reusable Make vars carry the redirection: -- `XCODEBUILD_CACHE_FLAGS` — `-derivedDataPath`, `-clonedSourcePackagesDirPath`,-  `-packageCachePath` (passed as xcodebuild arguments).-- `XCODEBUILD_ENV` — `XDG_CACHE_HOME`, `TMPDIR`, `CLANG_MODULE_CACHE_PATH`-  (exported before the xcodebuild command).+- `XCODEBUILD_CACHE_FLAGS` — `-derivedDataPath` plus+  `CLANG_MODULE_CACHE_PATH=<workspace path>` as an **xcodebuild build-setting+  argument**. Exporting the latter as an environment variable does not set+  Xcode's module-cache build setting.+- `XCODEBUILD_ENV` — `XDG_CACHE_HOME`, `TMPDIR`, and+  `SWIFTPM_MODULECACHE_OVERRIDE`. The SwiftPM override keeps manifest compiler+  modules and `*.dia` diagnostics out of `~/Library/Caches/org.swift.swiftpm`.++`-clonedSourcePackagesDirPath` and `-packageCachePath` are intentionally+omitted. Their subdirectory values disable Xcode's normal package-resolution+path; `-derivedDataPath` keeps Xcode-managed `SourcePackages` workspace-local.  A `prepare-cache-dirs` prerequisite target creates the redirected directories-before every build/clean/test. `-derivedDataPath` alone is not sufficient-because SwiftPM uses a separate package cache and Clang falls back to-`$XDG_CACHE_HOME/clang/ModuleCache` when invoked without-`-fmodules-cache-path` (which SwiftPM resolution does).--When adding a new xcodebuild target, include `prepare-cache-dirs` as a-prerequisite and prefix the command with `$(XCODEBUILD_ENV)` and-`$(XCODEBUILD_CACHE_FLAGS)`.+before every source-building xcodebuild target. When adding one, include+`prepare-cache-dirs` and prefix the command with `$(XCODEBUILD_ENV)` and+`$(XCODEBUILD_CACHE_FLAGS)`. The shell guard at+`tests/makefile/test_workspace_local_caches.sh` enforces these controls and+checks that the intentionally omitted package flags do not return.  ## Test Infrastructure 
specs/bugfixes/test-quick-workspace-local-cache-regression/report.md Added +153 / -0
diff --git a/specs/bugfixes/test-quick-workspace-local-cache-regression/report.md b/specs/bugfixes/test-quick-workspace-local-cache-regression/report.mdnew file mode 100644index 0000000..069ad43--- /dev/null+++ b/specs/bugfixes/test-quick-workspace-local-cache-regression/report.md@@ -0,0 +1,153 @@+# Bugfix Report: Test-Quick Workspace-Local Cache Regression++**Date:** 2026-08-04+**Status:** Fixed+**Transit ticket:** T-1628++## Description of the Issue++`make test-quick` could fail before compiling tests in restricted-home agent+sessions. SwiftPM manifest compilation attempted to write diagnostics below+`~/Library/Caches/org.swift.swiftpm`, and Clang module loading could write below+`~/.cache/clang/ModuleCache`.++The accompanying shell guard was also stale: it required+`-clonedSourcePackagesDirPath` and `-packageCachePath`, although the Makefile+correctly documented that those subdirectory arguments disable Xcode's ordinary+package-resolution path.++**Reproduction steps:**+1. Run `bash tests/makefile/test_workspace_local_caches.sh`.+2. Observe the stale requirement for `-clonedSourcePackagesDirPath` fail despite+   the documented intentional omission.+3. Run `make test-quick` in an environment that disallows user-global cache+   writes.+4. Observe SwiftPM manifest diagnostics or Clang module-cache permission errors+   before test execution.++**Impact:** Sandbox and agent invocations could not reliably resolve packages+or begin the fast macOS test suite. The stale guard both failed locally and+would have encouraged restoring package-resolution-breaking flags.++## Investigation Summary++- **Symptoms examined:** T-1628's reported failures at+  `~/.cache/clang/ModuleCache/Swift-*.swiftmodule` and+  `~/Library/Caches/org.swift.swiftpm/manifests/ManifestLoading/*.dia`; the+  current guard's failure for the intentionally omitted package flags.+- **Code inspected:** `Makefile`,+  `tests/makefile/test_workspace_local_caches.sh`, the T-1241 history, and+  `docs/agent-notes/project-structure.md`.+- **Hypotheses tested:** `xcodebuild -showBuildSettings` showed that exporting+  `CLANG_MODULE_CACHE_PATH` does not change Xcode's+  `CLANG_MODULE_CACHE_PATH` build setting. Passing it as a command-line build+  setting does. The installed `swift-package` binary contains+  `SWIFTPM_MODULECACHE_OVERRIDE`, confirming that SwiftPM supports the manifest+  module-cache override.++## Discovered Root Cause++T-1241 exported `CLANG_MODULE_CACHE_PATH` as a process environment variable,+but Xcode consumes it as a build setting and therefore retained its default+user-global module cache. It also did not set SwiftPM's+`SWIFTPM_MODULECACHE_OVERRIDE`, leaving manifest compiler modules and `.dia`+diagnostics under the user cache. The regression shell script had not been+updated when the deliberately harmful package-cache flags were removed.++**Defect type:** Misconfigured tool integration and stale regression coverage.++**Why it occurred:** The first cache redirection treated all cache controls as+environment variables or xcodebuild flags without distinguishing Xcode build+settings from SwiftPM environment overrides. A later package-resolution repair+changed the Makefile but not the guard or project note.++**Contributing factors:** A restricted synthetic `HOME` cannot fully reproduce+the harness's Xcode sandbox policy, so the structural guard must assert the+actual supported controls as well as runtime package resolution.++## Resolution for the Issue++**Changes made:**+- `Makefile` - passes `CLANG_MODULE_CACHE_PATH` as an xcodebuild build-setting+  argument; exports `SWIFTPM_MODULECACHE_OVERRIDE` to a workspace-local+  directory; retains `XDG_CACHE_HOME`, `TMPDIR`, and `-derivedDataPath`; removes+  unused package-cache directory setup.+- `tests/makefile/test_workspace_local_caches.sh` - replaces obsolete package+  flag requirements with assertions for the supported controls, their exact+  workspace-local paths, the intentional package-flag omission, all relevant+  source-building targets, preparation, and cleanup.+- `docs/agent-notes/project-structure.md` - documents the build-setting versus+  environment-variable distinction and why package flags remain absent.++**Approach rationale:** `-derivedDataPath` lets Xcode own package checkouts and+repository state below the workspace without disrupting resolution.+`CLANG_MODULE_CACHE_PATH=<path>` is the supported xcodebuild build-setting that+produces Clang's `-fmodules-cache-path`; `SWIFTPM_MODULECACHE_OVERRIDE` handles+SwiftPM manifest compilation and diagnostics before an Xcode target builds.++**Alternatives considered:**+- Re-add `-clonedSourcePackagesDirPath` and `-packageCachePath` - rejected:+  their subdirectory paths prevent normal Xcode package resolution.+- Keep `CLANG_MODULE_CACHE_PATH` as an environment variable - rejected:+  `xcodebuild -showBuildSettings` proved it leaves Xcode's module-cache setting+  at its user-global default.+- Redirect the full `HOME` - rejected: this is broader than cache isolation and+  would change credentials, tool configuration, and other unrelated behavior.++## Regression Test++**Test file:** `tests/makefile/test_workspace_local_caches.sh`++**What it verifies:** Every relevant Makefile target uses workspace-local+`XDG_CACHE_HOME`, `TMPDIR`, `SWIFTPM_MODULECACHE_OVERRIDE`, and+`CLANG_MODULE_CACHE_PATH`; the latter appears as an xcodebuild build-setting;+`-derivedDataPath` is retained; package-resolution-breaking flags are absent;+and preparation plus cleanup cover the local directories.++**Run command:** `bash tests/makefile/test_workspace_local_caches.sh`++## Affected Files++| File | Change |+|---|---|+| `Makefile` | Correct cache controls and local directories. |+| `tests/makefile/test_workspace_local_caches.sh` | Supported-control regression coverage. |+| `docs/agent-notes/project-structure.md` | Updated cache configuration guidance. |+| `CHANGELOG.md` | Unreleased T-1628 fix entry. |+| `specs/bugfixes/test-quick-workspace-local-cache-regression/report.md` | Investigation and resolution record. |++## Verification++**Automated:**+- [x] The rewritten guard failed before the Makefile fix with+  `build-ios does not set SWIFTPM_MODULECACHE_OVERRIDE`.+- [x] `bash tests/makefile/test_workspace_local_caches.sh` passes after the+  fix.+- [x] `make lint` passes.+- [x] A clean restricted-HOME unsigned `make build` passes for iOS Simulator+  and macOS, including package resolution.+- [x] A restricted-HOME `make clean` succeeds and removes `DerivedData`.+- [ ] `make test-quick` cannot complete on this machine: its unsigned macOS+  test runner aborts before bootstrapping. With Xcode signing disabled for+  validation, compilation and package resolution complete and Clang receives+  the workspace-local `-fmodules-cache-path`.++**Manual verification:**+- [x] `xcodebuild -showBuildSettings` resolves the explicit+  `CLANG_MODULE_CACHE_PATH` to the workspace path rather than its default+  user-global path.+- [x] The restricted-HOME build log shows package checkout and Clang commands+  writing under `DerivedData`, with no user-cache permission errors.++## Prevention++- Treat Xcode build settings and SwiftPM environment overrides as separate+  interfaces; do not assume an environment variable configures both tools.+- Keep dry-run Makefile guards focused on documented, supported controls and+  assert intentional omissions as well as required options.+- Run the cache guard whenever adding a source-building xcodebuild target.++## Related++- Transit ticket T-1628+- T-1241, the original workspace-cache redirection change
tests/makefile/test_workspace_local_caches.sh Modified +57 / -92
diff --git a/tests/makefile/test_workspace_local_caches.sh b/tests/makefile/test_workspace_local_caches.shindex 8e245fc..4dc6c81 100755--- a/tests/makefile/test_workspace_local_caches.sh+++ b/tests/makefile/test_workspace_local_caches.sh@@ -1,14 +1,11 @@ #!/usr/bin/env bash-# Regression test for T-1241: Make build-macos should use workspace-local Xcode caches.+# Regression test for T-1628: every source-building xcodebuild target must+# keep Clang modules and SwiftPM manifest diagnostics/cache writes workspace-local. #-# In sandboxed/dev environments, xcodebuild fails when it tries to write to-# user-global caches (~/.cache/clang/ModuleCache, ~/Library/Caches/org.swift.swiftpm).-# The Makefile must redirect every cache path that xcodebuild and its subprocesses-# touch into the workspace.-#-# This test inspects `make -n build-macos` (dry-run) and `make -n clean` output and-# verifies that the required cache-redirection flags and environment variables are-# present. It does not actually build the project.+# `-clonedSourcePackagesDirPath` and `-packageCachePath` are deliberately absent:+# their subdirectory values prevent normal xcodebuild package resolution. Xcode+# owns SourcePackages below -derivedDataPath. This guard checks the supported+# controls instead, without invoking xcodebuild.  set -euo pipefail @@ -24,96 +21,93 @@ pass() {     echo "PASS: $*" } -# Use make's dry-run mode so we get the resolved command lines without executing.-build_cmds="$(make -n build-macos 2>/dev/null)"-build_ios_cmds="$(make -n build-ios 2>/dev/null)"-clean_cmds="$(make -n clean 2>/dev/null)"-test_cmds="$(make -n test-quick 2>/dev/null)"--# 1. SwiftPM clones directory must be workspace-local.-echo "$build_cmds" | grep -q -- '-clonedSourcePackagesDirPath' \-    || fail "build-macos does not pass -clonedSourcePackagesDirPath"-pass "build-macos passes -clonedSourcePackagesDirPath"--# 2. SwiftPM package cache must be workspace-local.-echo "$build_cmds" | grep -q -- '-packageCachePath' \-    || fail "build-macos does not pass -packageCachePath"-pass "build-macos passes -packageCachePath"--# 3. XDG_CACHE_HOME must be redirected so Clang's default module cache-#    (~/.cache/clang/ModuleCache when -fmodules-cache-path is not passed) stays-#    in the workspace.-echo "$build_cmds" | grep -q 'XDG_CACHE_HOME=' \-    || fail "build-macos does not export XDG_CACHE_HOME"-pass "build-macos exports XDG_CACHE_HOME"--# 4. TMPDIR must be redirected so compiler temp diagnostics (*.dia) don't escape-#    the workspace.-echo "$build_cmds" | grep -q 'TMPDIR=' \-    || fail "build-macos does not export TMPDIR"-pass "build-macos exports TMPDIR"--# 5. None of the redirected paths may resolve to a user-global cache location.-#    Workspace-relative absolute paths inside the repo are fine; what we forbid-#    is the well-known global caches (~/.cache, ~/Library/Caches) that triggered-#    the original sandbox failure. workspace_root="$(pwd)"-for var in XDG_CACHE_HOME TMPDIR CLANG_MODULE_CACHE_PATH; do-    value_line="$(echo "$build_cmds" | grep -oE "${var}=[^ ]+" | head -1 || true)"-    [ -n "$value_line" ] || fail "could not extract $var from build-macos commands"-    value="${value_line#${var}=}"-    case "$value" in-        \$HOME*|\$\{HOME\}*|~*)-            fail "$var resolves to a user-global path: $value"-            ;;-        *.cache|*.cache/*|*Library/Caches|*Library/Caches/*)-            fail "$var resolves to a user-global cache path: $value"-            ;;-    esac-    case "$value" in-        "$workspace_root"/*)-            ;;-        /*)-            fail "$var ($value) is absolute but not inside the workspace ($workspace_root)"-            ;;-    esac+workspace_cache="$workspace_root/DerivedData/Caches"+workspace_tmp="$workspace_root/DerivedData/tmp"+manifest_module_cache="$workspace_cache/org.swift.swiftpm"+clang_module_cache="$workspace_root/DerivedData/ModuleCache.noindex"++assert_workspace_path() {+    local target="$1"+    local variable="$2"+    local expected="$3"+    local commands="$4"+    local value_line++    value_line="$(printf '%s\n' "$commands" | grep -oE "${variable}=[^[:space:]]+" | head -1 || true)"+    [ -n "$value_line" ] || fail "$target does not set $variable"+    [ "$value_line" = "${variable}=${expected}" ] \+        || fail "$target sets $variable to ${value_line#${variable}=}, not $expected"+    pass "$target sets $variable inside the workspace"+}++assert_xcodebuild_setting() {+    local target="$1"+    local setting="$2"+    local commands="$3"+    local xcodebuild_arguments++    xcodebuild_arguments="${commands#*xcodebuild }"+    [ "$xcodebuild_arguments" != "$commands" ] \+        || fail "$target does not invoke xcodebuild"+    printf '%s\n' "$xcodebuild_arguments" | grep -Fq -- "$setting" \+        || fail "$target does not pass $setting as an xcodebuild build setting"+    pass "$target passes $setting as an xcodebuild build setting"+}++assert_cache_controls() {+    local target="$1"+    local commands="$2"++    printf '%s\n' "$commands" | grep -Fq -- 'set -o pipefail;' \+        || fail "$target does not enable pipefail before its xcodebuild pipeline"+    printf '%s\n' "$commands" | grep -Fq -- '-derivedDataPath ./DerivedData' \+        || fail "$target does not pass -derivedDataPath ./DerivedData"+    assert_xcodebuild_setting "$target" "CLANG_MODULE_CACHE_PATH=$clang_module_cache" "$commands"+    assert_workspace_path "$target" XDG_CACHE_HOME "$workspace_cache" "$commands"+    assert_workspace_path "$target" TMPDIR "$workspace_tmp" "$commands"+    assert_workspace_path "$target" SWIFTPM_MODULECACHE_OVERRIDE "$manifest_module_cache" "$commands"++    printf '%s\n' "$commands" | grep -Fq -- '-clonedSourcePackagesDirPath' \+        && fail "$target must omit -clonedSourcePackagesDirPath so xcodebuild can resolve packages"+    printf '%s\n' "$commands" | grep -Fq -- '-packageCachePath' \+        && fail "$target must omit -packageCachePath so xcodebuild can resolve packages"+    pass "$target uses supported cache controls and preserves package resolution"+}++make_dry_run() {+    local target="$1"++    # DEVICE_ID is a recursively-expanded $(shell ...) value. Override it for+    # install so make -n only prints the recipe and never enumerates devices.+    if [ "$target" = "install" ]; then+        make -n "$target" DEVICE_ID=cache-guard-device-id 2>/dev/null+    else+        make -n "$target" 2>/dev/null+    fi+}++# Use make's dry-run mode so we get resolved command lines without executing.+for target in build-ios build-macos test-quick test test-ui install archive clean; do+    commands="$(make_dry_run "$target")"+    assert_cache_controls "$target" "$commands"+done++prepare_cmds="$(make -n prepare-cache-dirs 2>/dev/null)"+for path in \+    ./DerivedData/Caches \+    ./DerivedData/tmp \+    ./DerivedData/Caches/org.swift.swiftpm \+    ./DerivedData/ModuleCache.noindex; do+    printf '%s\n' "$prepare_cmds" | grep -Fq -- "$path" \+        || fail "prepare-cache-dirs does not create $path" done-pass "build-macos cache paths are workspace-local"--# 6. The clean target must also use the redirected caches so it doesn't fail-#    before the build can run.-echo "$clean_cmds" | grep -q -- '-clonedSourcePackagesDirPath' \-    || fail "clean does not pass -clonedSourcePackagesDirPath"-echo "$clean_cmds" | grep -q -- '-packageCachePath' \-    || fail "clean does not pass -packageCachePath"-echo "$clean_cmds" | grep -q 'XDG_CACHE_HOME=' \-    || fail "clean does not export XDG_CACHE_HOME"-echo "$clean_cmds" | grep -q 'TMPDIR=' \-    || fail "clean does not export TMPDIR"-pass "clean target redirects caches"--# 7. Test targets should also share the redirection so test runs don't regress-#    in sandboxed environments.-echo "$test_cmds" | grep -q -- '-clonedSourcePackagesDirPath' \-    || fail "test-quick does not pass -clonedSourcePackagesDirPath"-echo "$test_cmds" | grep -q 'XDG_CACHE_HOME=' \-    || fail "test-quick does not export XDG_CACHE_HOME"-pass "test-quick redirects caches"--# 8. build-ios must use the same redirection as build-macos. iOS Simulator-#    builds invoke the same SwiftPM/Clang subprocesses as macOS, so the same-#    sandbox-unsafe cache paths apply.-echo "$build_ios_cmds" | grep -q -- '-clonedSourcePackagesDirPath' \-    || fail "build-ios does not pass -clonedSourcePackagesDirPath"-echo "$build_ios_cmds" | grep -q -- '-packageCachePath' \-    || fail "build-ios does not pass -packageCachePath"-echo "$build_ios_cmds" | grep -q 'XDG_CACHE_HOME=' \-    || fail "build-ios does not export XDG_CACHE_HOME"-echo "$build_ios_cmds" | grep -q 'TMPDIR=' \-    || fail "build-ios does not export TMPDIR"-echo "$build_ios_cmds" | grep -q 'CLANG_MODULE_CACHE_PATH=' \-    || fail "build-ios does not export CLANG_MODULE_CACHE_PATH"-pass "build-ios redirects caches"+pass "prepare-cache-dirs creates every workspace-local cache directory"++clean_cmds="$(make -n clean 2>/dev/null)"+printf '%s\n' "$clean_cmds" | grep -Fq -- 'rm -rf ./DerivedData build' \+    || fail "clean does not remove workspace-local cache artifacts"+pass "clean removes workspace-local cache artifacts"  echo echo "All workspace-local cache redirection checks passed."

Things to double-check

Host signing capability

Re-run make test-quick on a macOS host with a Mac Development signing certificate to obtain the missing end-to-end test execution. The reviewed host reached only that credential failure after cache/package setup.

SwiftPM toolchain upgrades

Because SWIFTPM_MODULECACHE_OVERRIDE is a SwiftPM control verified against the installed toolchain, rerun the restricted-HOME build and guard after a major Xcode/SwiftPM upgrade.