Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Prefer the developer daemon as the default way to build, run, and validate. Four

- **Lane-serialized job queue** — every job claims named lanes (`build`, `debugArtifact`, `liveApp`, `release`, `style`); the daemon runs jobs that share a lane one at a time while letting unrelated lanes proceed concurrently. That serial queue is what stops multiple agents from building, launching, or running style tooling over each other and corrupting `.build` or the live app.
- **Machine-wide heavy-job slots** — Swift/Xcode-heavy daemon jobs acquire per-user global heavy admission under `/tmp/repoprompt-ce-dev-locks-<uid>/`, independent of daemon socket overrides. The default capacity is 1; set `REPOPROMPT_DEV_HEAVY_SLOTS=N` explicitly to allow more concurrent heavy jobs. If `job status` or `job wait` shows `global-wait` or "waiting for global heavy slot", another checkout/worktree is already running heavy work; wait on the ticket instead of bypassing conductor with direct `swift`/`xcodebuild` commands or starting duplicate jobs. Holder metadata is display-only and identifies the operation/ticket/repo/worktree when available.
- **Opt-in bounded Swift parallelism** — by default SwiftPM uses the active processor count for build/test jobs. Set `REPOPROMPT_SWIFT_JOBS=N` (a positive decimal integer, no leading zeros or signs) to pass `--jobs N` to coordinated `swift build` and `swift test` invocations, including `make dev-build`, `make dev-swift-build`, `make dev-test`, `make dev-provider-test`, and `make dev-run`. The same value is honored by `Scripts/package_app.sh` and `Scripts/build_swiftpm_release_products.sh`. This is independent of `REPOPROMPT_DEV_HEAVY_SLOTS`; a per-job limit is not a machine-wide heavy slot. The requested/effective limit is part of the daemon env snapshot and `request-key` fingerprint, so jobs with different limits are not reused, and the limit is echoed in the command log and `Phases` summary. When unset, SwiftPM's default behavior is unchanged.
- **Machine-wide live-app lock** — debug app stop/launch/relaunch/smoke-launch lifecycle work acquires a separate per-user `live-app.lock`, so worktree-local daemons cannot mutate the singleton debug app at the same time.
- **Tickets + async jobs** — every job gets a ticket and can run detached (`--async`). Fire a build, keep working, and query or wait on it later (`job status` / `job wait`) instead of blocking on a long compile. Jobs survive reconnects and are reusable by `--request-key`.

Expand Down
7 changes: 7 additions & 0 deletions Scripts/build_swiftpm_release_products.sh
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ SWIFT_BUILD_ARGS=(-c release)
if sentry_linking_enabled; then
SWIFT_BUILD_ARGS+=(-debug-info-format dwarf)
fi
if [[ -n "${REPOPROMPT_SWIFT_JOBS:-}" ]]; then
if [[ "$REPOPROMPT_SWIFT_JOBS" =~ ^[1-9][0-9]*$ ]]; then
SWIFT_BUILD_ARGS+=("--jobs" "$REPOPROMPT_SWIFT_JOBS")
else
fail "REPOPROMPT_SWIFT_JOBS must be a positive integer"
fi
fi

ARM64_BIN_DIR=""
X86_64_BIN_DIR=""
Expand Down
53 changes: 49 additions & 4 deletions Scripts/conductor.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
socket default: /tmp/conductor-<uid>/<repo-root-hash16>.sock (directory mode 0700)
machine locks: /tmp/repoprompt-ce-dev-locks-<uid>/ (directory mode 0700; independent of socket overrides)
heavy slots: REPOPROMPT_DEV_HEAVY_SLOTS=N (default 1)
swift jobs: REPOPROMPT_SWIFT_JOBS=N (opt-in; when unset, SwiftPM uses the active processor count)
overrides: REPOPROMPT_DEV_DAEMON_STATE_DIR, REPOPROMPT_DEV_DAEMON_SOCKET (socket parent must be owned 0700)

Protocol version: {PROTOCOL_VERSION}
Expand Down Expand Up @@ -411,6 +412,25 @@ def global_heavy_slot_paths(env: Optional[Dict[str, str]] = None) -> List[Path]:
return [root / f"global-heavy-{index}.lock" for index in range(configured_global_heavy_slots(env))]


def configured_swift_jobs(env: Optional[Dict[str, str]] = None) -> Optional[int]:
source = env if env is not None else os.environ
raw = source.get("REPOPROMPT_SWIFT_JOBS")
if raw is None or raw == "":
return None
# Match the shell validation in package_app.sh and build_swiftpm_release_products.sh:
# a positive decimal integer with no leading zeros and no sign.
if not re.fullmatch(r"[1-9][0-9]*", raw):
raise ConductorError("REPOPROMPT_SWIFT_JOBS must be a positive integer")
return int(raw)


def append_swift_jobs_arg(argv: List[str], env: Optional[Dict[str, str]] = None) -> List[str]:
jobs = configured_swift_jobs(env)
if jobs is None:
return argv
return argv + ["--jobs", str(jobs)]


def live_app_lock_path() -> Path:
return machine_lock_dir() / "live-app.lock"

Expand Down Expand Up @@ -1256,6 +1276,7 @@ class OperationRegistry:
]
CONDUCTOR_ENV_KEYS = [
"REPOPROMPT_DEV_HEAVY_SLOTS",
"REPOPROMPT_SWIFT_JOBS",
]
TELEMETRY_ENV_KEYS = [
"REPOPROMPT_ENABLE_SENTRY",
Expand Down Expand Up @@ -1353,7 +1374,8 @@ def prepare(self, request: Dict[str, Any]) -> Tuple[List[str], List[str], Path,
lanes = ["build"]
if product == "all":
return self._internal_argv("swift_build_all", {}), lanes, cwd, env, effective_timeout
return ["swift", "build", "--product", str(product)], lanes, cwd, env, effective_timeout
argv = ["swift", "build", "--product", str(product)]
return append_swift_jobs_arg(argv, env), lanes, cwd, env, effective_timeout
if operation == "build":
return [script("package_app.sh"), "debug"], ["build", "debugArtifact"], cwd, env, effective_timeout
if operation == "package":
Expand All @@ -1368,7 +1390,7 @@ def prepare(self, request: Dict[str, Any]) -> Tuple[List[str], List[str], Path,
argv.append("list")
elif args.get("filter"):
argv.extend(["--filter", str(args["filter"])])
return argv, ["build"], cwd, env, effective_timeout
return append_swift_jobs_arg(argv, env), ["build"], cwd, env, effective_timeout
if operation == "provider-test":
argv = ["swift", "test"]
if args.get("testProduct"):
Expand All @@ -1377,7 +1399,7 @@ def prepare(self, request: Dict[str, Any]) -> Tuple[List[str], List[str], Path,
argv.append("list")
elif args.get("filter"):
argv.extend(["--filter", str(args["filter"])])
return argv, ["build"], self.repo_root / "Packages" / "RepoPromptAgentProviders", env, effective_timeout
return append_swift_jobs_arg(argv, env), ["build"], self.repo_root / "Packages" / "RepoPromptAgentProviders", env, effective_timeout
if operation == "install-debug-cli":
return [script("install_debug_cli.sh"), "install", "--build"], ["build", "debugArtifact"], cwd, env, effective_timeout
if operation == "debug-cli-status":
Expand Down Expand Up @@ -2024,9 +2046,20 @@ def _run_job(self, ticket: str) -> None:
global_heavy_slot = self._acquire_global_heavy_slot(job.ticket)
if global_heavy_slot is None:
return
# Only parse/validate the Swift jobs limit for SwiftPM-invoking operations.
# Packaging/build scripts read REPOPROMPT_SWIFT_JOBS themselves and must not
# be blocked here; sleep/diagnostics and other lanes must not be affected.
if job.operation in {"swift-build", "test", "provider-test"}:
swift_jobs_limit = configured_swift_jobs(env)
else:
swift_jobs_limit = None
start_line = f"$ {format_argv(argv)}\n"
with job.log_path.open("ab") as log_file:
with self.lock:
if swift_jobs_limit is not None:
header_line = f"==> Swift jobs limit: {swift_jobs_limit}\n"
self._append_tail_locked(job, header_line)
log_file.write(header_line.encode("utf-8", errors="replace"))
self._append_tail_locked(job, start_line)
log_file.write(start_line.encode("utf-8", errors="replace"))
log_file.flush()
Expand Down Expand Up @@ -3704,9 +3737,12 @@ def run_operation_command(
env: Optional[Dict[str, str]] = None,
allow_exit_codes: Optional[set[int]] = None,
timeout: Optional[float] = None,
swift_jobs_limit: Optional[int] = None,
) -> Tuple[int, str, str]:
allowed = allow_exit_codes if allow_exit_codes is not None else {0}
print(f"\n==> {name}", flush=True)
if swift_jobs_limit is not None:
print(f"==> Swift jobs limit: {swift_jobs_limit}", flush=True)
print(f"$ {format_argv(argv)}", flush=True)
try:
completed = subprocess.run(
Expand Down Expand Up @@ -4228,8 +4264,17 @@ def operation_app_stop(repo_root: Path, args: Dict[str, Any]) -> int:


def operation_swift_build_all(repo_root: Path) -> int:
swift_jobs_limit = configured_swift_jobs(os.environ)
for product in ["RepoPrompt", "repoprompt-mcp"]:
code, _stdout, _stderr = run_operation_command(f"swift build --product {product}", ["swift", "build", "--product", product], repo_root)
argv = ["swift", "build", "--product", product]
if swift_jobs_limit is not None:
argv.extend(["--jobs", str(swift_jobs_limit)])
code, _stdout, _stderr = run_operation_command(
f"swift build --product {product}",
argv,
repo_root,
swift_jobs_limit=swift_jobs_limit,
)
if code != 0:
return code
return 0
Expand Down
7 changes: 7 additions & 0 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ SWIFT_BUILD_ARGS=(-c "$CONF")
if sentry_linking_enabled; then
SWIFT_BUILD_ARGS+=(-debug-info-format dwarf)
fi
if [[ -n "${REPOPROMPT_SWIFT_JOBS:-}" ]]; then
if [[ "$REPOPROMPT_SWIFT_JOBS" =~ ^[1-9][0-9]*$ ]]; then
SWIFT_BUILD_ARGS+=("--jobs" "$REPOPROMPT_SWIFT_JOBS")
else
fail "REPOPROMPT_SWIFT_JOBS must be a positive integer"
fi
fi
PUBLIC_UNIVERSAL_RELEASE=0
ARCHITECTURE_POLICY="matching"
if (( IS_RELEASE )) && (( ! USE_LOCAL_SELF_SIGNED_RELEASE )); then
Expand Down
Loading
Loading