Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .agents/manifests/specs.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
id: specs
globs:
- .github/workflows/**
- .gitignore
- CHANGELOG.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
Expand All @@ -11,6 +13,7 @@ globs:
- examples/**/.simulator-broker/*.json
- spec/**
- references/**
- scripts/package_cli.sh
owner: spec-steward
required_skills:
- harness-engineering
Expand All @@ -24,7 +27,9 @@ primary_specs:
verification_profile: spec-only
commit_required: true
allowed_paths:
- .github/workflows/**
- .gitignore
- CHANGELOG.md
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- LICENSE
Expand All @@ -35,6 +40,7 @@ allowed_paths:
- examples/**/.simulator-broker/*.json
- spec/**
- references/**
- scripts/package_cli.sh
forbidden_paths: []
required_evidence:
- structured-git-commit
Expand Down
4 changes: 2 additions & 2 deletions .agents/verify/spec-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ covers_scenarios: []
covers_boundaries: []
commands:
- id: spec-diff-check
run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md docs spec .agents agent-harness package.json package-lock.json .gitignore .codex script scripts references app broker-core client && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md docs spec .agents agent-harness package.json package-lock.json .gitignore .codex script scripts references app broker-core client
run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client
- id: public-front-door-check
run: node --test docs/test/front-door.test.mjs
run: node --test docs/test/*.test.mjs
- id: instruction-parity-check
run: cmp -s AGENTS.md CLAUDE.md || { echo "AGENTS.md and CLAUDE.md must stay identical." >&2; exit 1; }
required_artifacts:
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Public Node test surface. Ubuntu, no npm install (the repo has no runtime
# dependencies), and no macOS app suite. timeout-minutes is 30 because a
# prior 20-minute budget was consumed by per-file git cat-file in
# verify:public-surface on a clean checkout.
name: Node tests

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: node-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
node:
name: broker-core client harness-adoption public-surface
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Public-surface scan
run: npm run verify:public-surface

- name: broker-core tests
run: npm run test:broker-core

- name: client tests
run: npm run test:client

- name: harness-adoption tests
run: npm run test:harness-adoption
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Publish the Alpha CLI tarball when a version tag is pushed.
# After merge, create the matching tag (example: v0.1.0-alpha.1).
# This workflow does not build or attach the macOS app.
name: Release CLI

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
release:
name: attach CLI tarball
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Require tag to match package.json
run: |
expected="v$(node -p "require('./package.json').version")"
if [[ "${GITHUB_REF_NAME}" != "$expected" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match package.json (${expected})." >&2
exit 1
fi

- name: Public Node test surface
run: |
npm run verify:public-surface
npm run test:broker-core
npm run test:client
npm run test:harness-adoption

- name: Package CLI tarball
run: npm run package:cli

- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version="$(node -p "require('./package.json').version")"
asset="artifacts/cli/simulator-broker-${version}-cli.tar.gz"
checksum="${asset}.sha256"
prerelease_args=()
if [[ "$version" == *alpha* || "$version" == *beta* || "$version" == *rc* ]]; then
prerelease_args+=(--prerelease)
fi
gh release create "${GITHUB_REF_NAME}" \
--title "Simulator Broker ${GITHUB_REF_NAME}" \
--notes "Alpha CLI tarball. Extract it and run \`./bin/simbroker --help\`. Node.js 20 or newer is required. macOS and Xcode are still required to create and run iOS Simulators. This release is not a Homebrew formula, notarized app, or npm package. See CHANGELOG.md." \
"${prerelease_args[@]}" \
"$asset" \
"$checksum"
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Changelog

All notable changes to Simulator Broker are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0-alpha.1] - 2026-08-18

First tagged Alpha. The CLI, local `brokerd` service, and macOS operator app
already exist on `main`; this release names that surface and attaches a
downloadable CLI tarball.

### Added

- Public Node test workflow on GitHub-hosted Ubuntu for
`verify:public-surface`, `test:broker-core`, `test:client`, and
`test:harness-adoption`. That job does not run the macOS app suite.
Snapshot tests inject the fixture `simctl` adapter so the suite does not
call host `xcrun`. The default public-surface scan skips identical index
blobs on a clean worktree, and the job budget is 30 minutes.
- `scripts/package_cli.sh` (`npm run package:cli`) builds a versioned CLI
tarball without XcodeGen or an app build.
- Tag-driven GitHub Release workflow that attaches the CLI tarball and its
SHA-256 checksum. Alpha tags are published as pre-releases.
- CLI-only install through `bash scripts/install_local.sh --cli-only`, with
PATH persistence through a Homebrew prefix bin or one guarded login-profile
snippet.
- Human-readable `simbroker` help and `simbroker doctor`, with `--json` for
machine payloads.
- Public-patches contributing track: Node.js 20 and the Node test suites, with
no harness session required.

### Notes

- Alpha: macOS and Xcode are still required to create and run iOS Simulators.
- This release is not a Homebrew formula, notarized app, or npm package.
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ npm run test:harness-adoption
```

App work also needs XcodeGen and `npm run test:app`. The full suite is
`npm test`.
`npm test`. The same Node suites run on GitHub-hosted Ubuntu CI. That job
does not run `npm run test:app`.

You do not need to run `agent:context`, `agent:verify`, or `agent:complete`,
and you do not need to create a task session directory.
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ A local control plane so humans, AI agents, and CI jobs can share iOS Simulators
on one Mac without stealing devices from each other.

**Status:** Alpha · macOS only · Xcode required
[![Node tests](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml/badge.svg)](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml)

Simulator Broker leases simulator aliases by *purpose* (for example
`agent-ui-session` or `manual-testing`) instead of hard-coding UDIDs. A local
Expand Down Expand Up @@ -37,8 +38,15 @@ Otherwise the installer writes `~/.local/bin/simbroker` and one guarded
login-shell PATH line. Open a new terminal if this shell still cannot resolve
`simbroker`. `source .../env.sh` remains a fallback.

Xcode is still required to create and run iOS Simulators. There is no Homebrew
formula, npm package, or GitHub Release yet.
To try a tagged build without cloning, download
`simulator-broker-<version>-cli.tar.gz` from
[Releases](https://github.com/fiveonecode/simulator-broker/releases), extract
it, and run `./bin/simbroker --help`. Node.js 20+ is still required.

Xcode is still required to create and run iOS Simulators. Alpha CLI tarballs
are attached to
[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases).
There is no Homebrew formula or npm package yet.

`simbroker` help and `simbroker doctor` print human-readable text by default.
Pass `--json` for machine-readable payloads.
Expand Down
7 changes: 4 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## Supported Versions

Security fixes are considered for the current `main` branch. Tagged release
support will be documented here when this project starts publishing versioned
binary or package releases.
Security fixes are considered for the current `main` branch and for the latest
tagged Alpha (`0.1.0-alpha.1`). Older Alpha tags are not supported. The
published artifact is the CLI tarball on GitHub Releases, not a Homebrew
formula, notarized app, or npm package.

## Reporting A Vulnerability

Expand Down
10 changes: 5 additions & 5 deletions broker-core/test/broker-core.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9350,7 +9350,7 @@ test("events and app snapshots honor a zero event limit", () => {
});

assert.equal(readEventsBroker(resolvedPaths, { limit: 0 }).events.length, 0);
assert.equal(appSnapshotBroker(resolvedPaths, { eventLimit: 0 }).recentEvents.length, 0);
assert.equal(appSnapshotBroker(resolvedPaths, runtimeOptions(paths, { eventLimit: 0 })).recentEvents.length, 0);
});

test("app snapshot reuses one process sample for active lease checks", () => {
Expand Down Expand Up @@ -9379,13 +9379,13 @@ test("app snapshot reuses one process sample for active lease checks", () => {
const liveSampler = liveProcessSampler({ command: "node broker-core.test.mjs", pid: process.pid });
let sampleCount = 0;

const snapshot = appSnapshotBroker(resolvedPaths, {
const snapshot = appSnapshotBroker(resolvedPaths, runtimeOptions(paths, {
processExists: (pid) => pid === process.pid,
processSampler: () => {
sampleCount += 1;
return liveSampler();
},
});
}));

assert.equal(snapshot.activeLeases.length, 2);
assert.equal(sampleCount, 1);
Expand Down Expand Up @@ -9422,7 +9422,7 @@ test("app snapshot reads only a bounded event tail for recent events", (t) => {
return originalReadFileSync.call(this, target, ...args);
};

const snapshot = appSnapshotBroker(resolvedPaths, { eventLimit: 3 });
const snapshot = appSnapshotBroker(resolvedPaths, runtimeOptions(paths, { eventLimit: 3 }));

assert.deepEqual(snapshot.recentEvents.map((event) => event.eventId), [
"event-119",
Expand Down Expand Up @@ -9661,7 +9661,7 @@ test("broker-owned state files are restricted to the current user", () => {
purposeId: "agent-ui-session",
simctlAdapter: paths.simctl.adapter,
});
writeAppSnapshotArtifact(resolvedPaths);
writeAppSnapshotArtifact(resolvedPaths, runtimeOptions(paths));

const statePaths = [
resolvedPaths.stateRoot,
Expand Down
23 changes: 22 additions & 1 deletion client/public-surface.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,24 @@ function defaultCandidateFiles(root) {
return output.split("\0").filter(Boolean);
}

// Clean checkouts match the index, so skip per-file `git cat-file`. A prior
// Ubuntu CI run spent 17 minutes spawning one process per tracked file.
function dirtyWorktreeFiles(root) {
try {
const output = execGit([
"diff-files",
"-z",
"--name-only",
], {
cwd: root,
encoding: "utf8",
});
return new Set(output.split("\0").filter(Boolean));
} catch {
return null;
}
}

function defaultCandidateIndexModes(root) {
const output = execGit([
"ls-files",
Expand Down Expand Up @@ -318,6 +336,7 @@ export function scanPublicSurface({
const candidateFiles = files ?? defaultCandidateFiles(resolvedRoot);
const scanIndexBlobs = files === undefined;
const indexModes = scanIndexBlobs ? defaultCandidateIndexModes(resolvedRoot) : new Map();
const dirtyFiles = scanIndexBlobs ? dirtyWorktreeFiles(resolvedRoot) : new Set();
const resolvedDenylistPath = denylistPath ?? path.join(resolvedRoot, LOCAL_DENYLIST_NAME);
const denylistRules = localDenylistRules(resolvedDenylistPath);
const builtInRules = [
Expand Down Expand Up @@ -421,6 +440,7 @@ export function scanPublicSurface({
if (!absoluteFile.startsWith(`${resolvedRoot}${path.sep}`)) {
continue;
}
let worktreeMissing = false;
try {
const fileStats = fs.lstatSync(absoluteFile);
if (fileStats.isSymbolicLink()) {
Expand All @@ -438,8 +458,9 @@ export function scanPublicSurface({
if (error?.code !== "ENOENT") {
throw error;
}
worktreeMissing = true;
}
if (scanIndexBlobs) {
if (scanIndexBlobs && (dirtyFiles === null || dirtyFiles.has(relativeFile) || worktreeMissing)) {
scanText(normalizedRelativeFile, indexBlobContent(resolvedRoot, relativeFile));
}
}
Expand Down
23 changes: 23 additions & 0 deletions client/test/public-surface.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,29 @@ test("default public surface candidates ignore untracked scratch files", () => {
}]);
});

test("default public surface scan inspects staged blobs when the worktree file is missing", () => {
const root = makeTempDir();
const localHome = path.join(root, "private-home");
execFileSync("git", ["init"], { cwd: root, stdio: "ignore" });
fs.writeFileSync(path.join(root, "README.md"), `machine path: ${localHome}/state\n`);
execFileSync("git", ["add", "README.md"], { cwd: root, stdio: "ignore" });
fs.rmSync(path.join(root, "README.md"));

const report = scanPublicSurface({
homePath: localHome,
root,
});

assert.equal(report.ok, false);
assert.equal(report.filesScanned, 1);
assert.deepEqual(report.issues, [{
line: 1,
path: "README.md",
rule: "local-home-path",
}]);
assert.equal(JSON.stringify(report).includes(localHome), false);
});

test("default public surface scan inspects staged blobs even after worktree cleanup", () => {
const root = makeTempDir();
const localHome = path.join(root, "private-home");
Expand Down
9 changes: 8 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ simbroker --help
This copies `broker-core`, `client`, and `package.json` into the install prefix
and writes a `simbroker` wrapper. It does not run XcodeGen or build the app.

To install from a tagged Alpha without cloning, download
`simulator-broker-<version>-cli.tar.gz` from
[GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases),
extract it, and run `./bin/simbroker --help`. That archive is the Node CLI
only.

- If Homebrew is present and `$(brew --prefix)/bin` is writable, the wrapper
is installed there so a new login shell already has it on `PATH`.
- Otherwise the wrapper is installed to `~/.local/bin` and the installer
Expand Down Expand Up @@ -135,7 +141,8 @@ SIMBROKER_DISTRIBUTION_SIGNING_IDENTITY='Developer ID Application: Example (TEAM
npm run package:distribution
```

No signed build is published on GitHub Releases yet.
GitHub Releases attach the Alpha CLI tarball. A signed, notarized app is not
published there yet.

## What to read next

Expand Down
Loading
Loading